From 4f34a4e4d81cb47f585ec1f614019837fc9b6cc4 Mon Sep 17 00:00:00 2001
From: "github-actions[bot]" <github-actions[bot]@users.noreply.github.com>
Date: Sun, 29 Sep 2024 11:42:26 +0000
Subject: [PATCH 001/121] [skip ci] Update CHANGELOG.md (prepend template)

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

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1fbb46786e..db969a63c2 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,15 @@
+## Unreleased
+
+### General
+-
+
+### Client
+-
+
+### Server
+-
+
+
 ## 2024.9.0
 
 ### General

From ca8cc015b0be1cc25d00d753be2fb3b26f4bfbd9 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=8B=E3=81=A3=E3=81=93=E3=81=8B=E3=82=8A?=
 <67428053+kakkokari-gtyih@users.noreply.github.com>
Date: Mon, 30 Sep 2024 20:05:34 +0900
Subject: [PATCH 002/121] =?UTF-8?q?enhance(frontend):=20=E3=83=95=E3=82=A9?=
 =?UTF-8?q?=E3=83=AD=E3=83=AF=E3=83=BC=E3=81=B8=E3=81=AE=E3=83=A1=E3=83=83?=
 =?UTF-8?q?=E3=82=BB=E3=83=BC=E3=82=B8=E6=AC=84=E3=82=92=E6=94=B9=E8=89=AF?=
 =?UTF-8?q?=20(#14656)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

* enhance(frontend): フォロワーへのメッセージ欄を改良

* Update Changelog
---
 CHANGELOG.md                                  |   2 +-
 locales/index.d.ts                            |   4 +
 locales/ja-JP.yml                             |   1 +
 .../frontend/src/components/MkFukidashi.vue   | 100 ++++++++++++++++++
 packages/frontend/src/pages/user/home.vue     |  21 +++-
 5 files changed, 123 insertions(+), 5 deletions(-)
 create mode 100644 packages/frontend/src/components/MkFukidashi.vue

diff --git a/CHANGELOG.md b/CHANGELOG.md
index db969a63c2..cfc07476e0 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,7 +4,7 @@
 -
 
 ### Client
--
+- Enhance: フォロワーへのメッセージ欄のデザイン改良
 
 ### Server
 -
diff --git a/locales/index.d.ts b/locales/index.d.ts
index 32c5a21648..29c93453ff 100644
--- a/locales/index.d.ts
+++ b/locales/index.d.ts
@@ -5148,6 +5148,10 @@ export interface Locale extends ILocale {
      * パスキーの検証に成功しましたが、パスワードレスログインが無効になっています。
      */
     "passkeyVerificationSucceededButPasswordlessLoginDisabled": string;
+    /**
+     * フォロワーへのメッセージ
+     */
+    "messageToFollower": string;
     "_delivery": {
         /**
          * 配信状態
diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml
index eebc4c995f..678af6987c 100644
--- a/locales/ja-JP.yml
+++ b/locales/ja-JP.yml
@@ -1283,6 +1283,7 @@ signinWithPasskey: "パスキーでログイン"
 unknownWebAuthnKey: "登録されていないパスキーです。"
 passkeyVerificationFailed: "パスキーの検証に失敗しました。"
 passkeyVerificationSucceededButPasswordlessLoginDisabled: "パスキーの検証に成功しましたが、パスワードレスログインが無効になっています。"
+messageToFollower: "フォロワーへのメッセージ"
 
 _delivery:
   status: "配信状態"
diff --git a/packages/frontend/src/components/MkFukidashi.vue b/packages/frontend/src/components/MkFukidashi.vue
new file mode 100644
index 0000000000..ba82eb442f
--- /dev/null
+++ b/packages/frontend/src/components/MkFukidashi.vue
@@ -0,0 +1,100 @@
+<!--
+SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-License-Identifier: AGPL-3.0-only
+-->
+
+<template>
+<div
+	:class="[
+		$style.root,
+		tail === 'left' ? $style.left : $style.right,
+		negativeMargin === true && $style.negativeMergin,
+		shadow === true && $style.shadow,
+	]"
+>
+	<div :class="$style.bg">
+		<svg v-if="tail !== 'none'" :class="$style.tail" version="1.1" viewBox="0 0 14.597 14.58" xmlns="http://www.w3.org/2000/svg">
+			<g transform="translate(-173.71 -87.184)">
+				<path d="m188.19 87.657c-1.469 2.3218-3.9315 3.8312-6.667 4.0865-2.2309-1.7379-4.9781-2.6816-7.8061-2.6815h-5.1e-4v12.702h12.702v-5.1e-4c2e-5 -1.9998-0.47213-3.9713-1.378-5.754 2.0709-1.6834 3.2732-4.2102 3.273-6.8791-6e-5 -0.49375-0.0413-0.98662-0.1235-1.4735z" fill-rule="evenodd" stroke-linecap="round" stroke-linejoin="round" stroke-width=".33225" style="paint-order:stroke fill markers"/>
+			</g>
+		</svg>
+		<div :class="$style.content">
+			<slot></slot>
+		</div>
+	</div>
+</div>
+</template>
+
+<script setup lang="ts">
+withDefaults(defineProps<{
+	tail?: 'left' | 'right' | 'none';
+	negativeMargin?: boolean;
+	shadow?: boolean;
+}>(), {
+	tail: 'right',
+	negativeMargin: false,
+	shadow: false,
+});
+</script>
+
+<style module lang="scss">
+.root {
+	--fukidashi-radius: var(--radius);
+	--fukidashi-bg: var(--panel);
+
+	position: relative;
+	display: inline-block;
+	min-height: calc(var(--fukidashi-radius) * 2);
+	padding-top: calc(var(--fukidashi-radius) * .13);
+
+	&.shadow {
+		filter: drop-shadow(0 4px 32px var(--shadow));
+	}
+
+	&.left {
+		padding-left: calc(var(--fukidashi-radius) * .13);
+
+		&.negativeMergin {
+			margin-left: calc(calc(var(--fukidashi-radius) * .13) * -1);
+		}
+	}
+
+	&.right {
+		padding-right: calc(var(--fukidashi-radius) * .13);
+
+		&.negativeMergin {
+			margin-right: calc(calc(var(--fukidashi-radius) * .13) * -1);
+		}
+	}
+}
+
+.bg {
+	width: 100%;
+	height: 100%;
+	background: var(--fukidashi-bg);
+	border-radius: var(--fukidashi-radius);
+}
+
+.content {
+	position: relative;
+	padding: 8px 12px;
+}
+
+.tail {
+	position: absolute;
+	top: 0;
+	display: block;
+	width: calc(var(--fukidashi-radius) * 1.13);
+	height: auto;
+	fill: var(--fukidashi-bg);
+}
+
+.left .tail {
+	left: 0;
+	transform: rotateY(180deg);
+}
+
+.right .tail {
+	right: 0;
+}
+</style>
diff --git a/packages/frontend/src/pages/user/home.vue b/packages/frontend/src/pages/user/home.vue
index ae8ac88361..93af534a9b 100644
--- a/packages/frontend/src/pages/user/home.vue
+++ b/packages/frontend/src/pages/user/home.vue
@@ -48,9 +48,10 @@ SPDX-License-Identifier: AGPL-3.0-only
 						</div>
 					</div>
 					<div v-if="user.followedMessage != null" class="followedMessage">
-						<div style="border: solid 1px var(--love); border-radius: 6px; background: color-mix(in srgb, var(--love), transparent 90%); padding: 6px 8px;">
-							<Mfm :text="user.followedMessage" :author="user"/>
-						</div>
+						<MkFukidashi class="fukidashi" :tail="narrow ? 'none' : 'left'" negativeMargin shadow>
+							<div class="messageHeader">{{ i18n.ts.messageToFollower }}</div>
+							<div><Mfm :text="user.followedMessage" :author="user"/></div>
+						</MkFukidashi>
 					</div>
 					<div v-if="user.roles.length > 0" class="roles">
 						<span v-for="role in user.roles" :key="role.id" v-tooltip="role.description" class="role" :style="{ '--color': role.color }">
@@ -161,6 +162,7 @@ import * as Misskey from 'misskey-js';
 import MkNote from '@/components/MkNote.vue';
 import MkFollowButton from '@/components/MkFollowButton.vue';
 import MkAccountMoved from '@/components/MkAccountMoved.vue';
+import MkFukidashi from '@/components/MkFukidashi.vue';
 import MkRemoteCaution from '@/components/MkRemoteCaution.vue';
 import MkTextarea from '@/components/MkTextarea.vue';
 import MkOmit from '@/components/MkOmit.vue';
@@ -467,7 +469,18 @@ onUnmounted(() => {
 
 				> .followedMessage {
 					padding: 24px 24px 0 154px;
-					font-size: 0.9em;
+
+					> .fukidashi {
+						display: block;
+						--fukidashi-bg: color-mix(in srgb, var(--love), var(--panel) 85%);
+						--fukidashi-radius: 16px;
+						font-size: 0.9em;
+
+						.messageHeader {
+							opacity: 0.7;
+							font-size: 0.85em;
+						}
+					}
 				}
 
 				> .roles {

From b6578861acf3002c8111b65850ec4ba45072b212 Mon Sep 17 00:00:00 2001
From: syuilo <4439005+syuilo@users.noreply.github.com>
Date: Mon, 30 Sep 2024 20:22:57 +0900
Subject: [PATCH 003/121] :art:

---
 packages/frontend/src/components/MkNoteHeader.vue | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/packages/frontend/src/components/MkNoteHeader.vue b/packages/frontend/src/components/MkNoteHeader.vue
index 888c570571..cf689d1fee 100644
--- a/packages/frontend/src/components/MkNoteHeader.vue
+++ b/packages/frontend/src/components/MkNoteHeader.vue
@@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <template>
 <header :class="$style.root">
-	<component :is="defaultStore.state.enableCondensedLine ? 'MkCondensedLine' : 'div'" :minScale="0.5" style="min-width: 0;">
+	<component :is="defaultStore.state.enableCondensedLine ? 'MkCondensedLine' : 'div'" :minScale="0.7" style="min-width: 0;">
 		<div style="display: flex; white-space: nowrap; align-items: baseline;">
 			<div v-if="mock" :class="$style.name">
 				<MkUserName :user="note.user"/>

From e9519b02fb1d3abbf97530a60ecb7963f079a925 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=8B=E3=81=A3=E3=81=93=E3=81=8B=E3=82=8A?=
 <67428053+kakkokari-gtyih@users.noreply.github.com>
Date: Tue, 1 Oct 2024 20:53:02 +0900
Subject: [PATCH 004/121] fix(misskey-js): build misskey-js with types (#14665)

---
 packages/misskey-js/src/autogen/types.ts | 5 +++++
 1 file changed, 5 insertions(+)

diff --git a/packages/misskey-js/src/autogen/types.ts b/packages/misskey-js/src/autogen/types.ts
index 0dff85183f..6aaeabec7b 100644
--- a/packages/misskey-js/src/autogen/types.ts
+++ b/packages/misskey-js/src/autogen/types.ts
@@ -5177,6 +5177,8 @@ export type operations = {
             urlPreviewRequireContentLength: boolean;
             urlPreviewUserAgent: string | null;
             urlPreviewSummaryProxyUrl: string | null;
+            federation: string;
+            federationHosts: string[];
           };
         };
       };
@@ -9428,6 +9430,9 @@ export type operations = {
           urlPreviewRequireContentLength?: boolean;
           urlPreviewUserAgent?: string | null;
           urlPreviewSummaryProxyUrl?: string | null;
+          /** @enum {string} */
+          federation?: 'all' | 'none' | 'specified';
+          federationHosts?: string[];
         };
       };
     };

From 6fd4de246c8ba0c59afe6f0c0a588d01783514fe Mon Sep 17 00:00:00 2001
From: Julia <julia@insertdomain.name>
Date: Wed, 2 Oct 2024 20:09:08 -0400
Subject: [PATCH 005/121] Make post form attachments accessible (#14666)

* fix(frontend): Make post form attachments accessible

Adds a role="button", tabindex, and @keydown to MkPostFormAttaches in
order to make it accessible to keyboard users.

* Fix for linter

* Add spacing in type signature
---
 .../src/components/MkPostFormAttaches.vue         | 15 +++++++++++++--
 1 file changed, 13 insertions(+), 2 deletions(-)

diff --git a/packages/frontend/src/components/MkPostFormAttaches.vue b/packages/frontend/src/components/MkPostFormAttaches.vue
index 80b75a0875..42322fec3d 100644
--- a/packages/frontend/src/components/MkPostFormAttaches.vue
+++ b/packages/frontend/src/components/MkPostFormAttaches.vue
@@ -7,7 +7,14 @@ SPDX-License-Identifier: AGPL-3.0-only
 <div v-show="props.modelValue.length != 0" :class="$style.root">
 	<Sortable :modelValue="props.modelValue" :class="$style.files" itemKey="id" :animation="150" :delay="100" :delayOnTouchOnly="true" @update:modelValue="v => emit('update:modelValue', v)">
 		<template #item="{element}">
-			<div :class="$style.file" @click="showFileMenu(element, $event)" @contextmenu.prevent="showFileMenu(element, $event)">
+			<div
+				:class="$style.file"
+				role="button"
+				tabindex="0"
+				@click="showFileMenu(element, $event)"
+				@keydown.space.enter="showFileMenu(element, $event)"
+				@contextmenu.prevent="showFileMenu(element, $event)"
+			>
 				<MkDriveFileThumbnail :data-id="element.id" :class="$style.thumbnail" :file="element" fit="cover"/>
 				<div v-if="element.isSensitive" :class="$style.sensitive">
 					<i class="ti ti-eye-exclamation" style="margin: auto;"></i>
@@ -133,7 +140,7 @@ async function crop(file: Misskey.entities.DriveFile): Promise<void> {
 	emit('replaceFile', file, newFile);
 }
 
-function showFileMenu(file: Misskey.entities.DriveFile, ev: MouseEvent): void {
+function showFileMenu(file: Misskey.entities.DriveFile, ev: MouseEvent | KeyboardEvent): void {
 	if (menuShowing) return;
 
 	const isImage = file.type.startsWith('image/');
@@ -199,6 +206,10 @@ function showFileMenu(file: Misskey.entities.DriveFile, ev: MouseEvent): void {
 	border-radius: 4px;
 	overflow: hidden;
 	cursor: move;
+
+	&:focus-visible {
+		outline-offset: 4px;
+	}
 }
 
 .thumbnail {

From a25d83f2499898d15ed4907ca67a0fd67822736c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E7=94=98=E7=80=AC=E3=81=93=E3=81=93=E3=81=82?=
 <amase.cocoa@gmail.com>
Date: Thu, 3 Oct 2024 09:09:37 +0900
Subject: [PATCH 006/121] =?UTF-8?q?fix:=20sass=E3=81=AEmodern-compiler?=
 =?UTF-8?q?=E3=82=92=E4=BD=BF=E3=81=86=E3=82=88=E3=81=86=E3=81=AB=20(#1465?=
 =?UTF-8?q?1)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

* fix(frontend-embed): ビルド時にsassのmodern-compilerを使うように

* fix(frontend): ビルド時にsassのmodern-compilerを使うように
---
 packages/frontend-embed/vite.config.ts | 5 +++++
 packages/frontend/vite.config.ts       | 5 +++++
 2 files changed, 10 insertions(+)

diff --git a/packages/frontend-embed/vite.config.ts b/packages/frontend-embed/vite.config.ts
index 64e67401c2..2dbee488c5 100644
--- a/packages/frontend-embed/vite.config.ts
+++ b/packages/frontend-embed/vite.config.ts
@@ -91,6 +91,11 @@ export function getConfig(): UserConfig {
 					}
 				},
 			},
+			preprocessorOptions: {
+				scss: {
+					api: 'modern-compiler',
+				},
+			},
 		},
 
 		define: {
diff --git a/packages/frontend/vite.config.ts b/packages/frontend/vite.config.ts
index e982df8ffd..504562a91e 100644
--- a/packages/frontend/vite.config.ts
+++ b/packages/frontend/vite.config.ts
@@ -109,6 +109,11 @@ export function getConfig(): UserConfig {
 					}
 				},
 			},
+			preprocessorOptions: {
+				scss: {
+					api: 'modern-compiler',
+				},
+			},
 		},
 
 		define: {

From 6dde45745294c171d17b60971353e664846a7a57 Mon Sep 17 00:00:00 2001
From: anatawa12 <anatawa12@icloud.com>
Date: Thu, 3 Oct 2024 09:24:22 +0900
Subject: [PATCH 007/121] Misskey js autogen check improvements (#14652)

* ci: Make failure if misskey js autogen detected changes

* ci: set persist-credentials
---
 .github/workflows/check-misskey-js-autogen.yml | 8 +++++++-
 1 file changed, 7 insertions(+), 1 deletion(-)

diff --git a/.github/workflows/check-misskey-js-autogen.yml b/.github/workflows/check-misskey-js-autogen.yml
index 5afd7d2714..f26c9a4d45 100644
--- a/.github/workflows/check-misskey-js-autogen.yml
+++ b/.github/workflows/check-misskey-js-autogen.yml
@@ -21,6 +21,7 @@ jobs:
         uses: actions/checkout@v4.1.1
         with:
           submodules: true
+          persist-credentials: false
           ref: refs/pull/${{ github.event.pull_request.number }}/merge
 
       - name: setup pnpm
@@ -57,7 +58,7 @@ jobs:
           name: generated-misskey-js
           path: packages/misskey-js/generator/built/autogen
 
-  # pull_request_target safety: permissions: read-all, and there are no secrets used in this job
+  # pull_request_target safety: permissions: read-all, and no user codes are executed
   get-actual-misskey-js:
     runs-on: ubuntu-latest
     permissions:
@@ -68,6 +69,7 @@ jobs:
         uses: actions/checkout@v4.1.1
         with:
           submodules: true
+          persist-credentials: false
           ref: refs/pull/${{ github.event.pull_request.number }}/merge
 
       - name: Upload From Merged
@@ -131,3 +133,7 @@ jobs:
           mode: delete
           message: "Thank you!"
           create_if_not_exists: false
+
+      - name: Make failure if changes are detected
+        if: steps.check-changes.outputs.changes == 'true'
+        run: exit 1

From 1074d625ed1d651702aca1016cad165e256bab29 Mon Sep 17 00:00:00 2001
From: syuilo <4439005+syuilo@users.noreply.github.com>
Date: Thu, 3 Oct 2024 12:11:09 +0900
Subject: [PATCH 008/121] enhance: require captcha for signin (#14655)

* wip

* Update MkSignin.vue

* Update MkSignin.vue

* wip

* Update CHANGELOG.md
---
 CHANGELOG.md                                  |  2 +-
 .../src/server/api/SigninApiService.ts        | 37 +++++++++++++++++++
 packages/frontend/src/components/MkSignin.vue | 35 +++++++++++++++++-
 .../src/components/MkSignupDialog.form.vue    |  4 +-
 4 files changed, 74 insertions(+), 4 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index cfc07476e0..8f0fd24c44 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,7 +1,7 @@
 ## Unreleased
 
 ### General
--
+- Enhance: セキュリティ向上のため、サインイン時もCAPTCHAを求めるようになりました
 
 ### Client
 - Enhance: フォロワーへのメッセージ欄のデザイン改良
diff --git a/packages/backend/src/server/api/SigninApiService.ts b/packages/backend/src/server/api/SigninApiService.ts
index edac9b3beb..2ccc75da00 100644
--- a/packages/backend/src/server/api/SigninApiService.ts
+++ b/packages/backend/src/server/api/SigninApiService.ts
@@ -9,6 +9,7 @@ import * as OTPAuth from 'otpauth';
 import { IsNull } from 'typeorm';
 import { DI } from '@/di-symbols.js';
 import type {
+	MiMeta,
 	SigninsRepository,
 	UserProfilesRepository,
 	UsersRepository,
@@ -20,6 +21,8 @@ import { IdService } from '@/core/IdService.js';
 import { bindThis } from '@/decorators.js';
 import { WebAuthnService } from '@/core/WebAuthnService.js';
 import { UserAuthService } from '@/core/UserAuthService.js';
+import { CaptchaService } from '@/core/CaptchaService.js';
+import { FastifyReplyError } from '@/misc/fastify-reply-error.js';
 import { RateLimiterService } from './RateLimiterService.js';
 import { SigninService } from './SigninService.js';
 import type { AuthenticationResponseJSON } from '@simplewebauthn/types';
@@ -31,6 +34,9 @@ export class SigninApiService {
 		@Inject(DI.config)
 		private config: Config,
 
+		@Inject(DI.meta)
+		private meta: MiMeta,
+
 		@Inject(DI.usersRepository)
 		private usersRepository: UsersRepository,
 
@@ -45,6 +51,7 @@ export class SigninApiService {
 		private signinService: SigninService,
 		private userAuthService: UserAuthService,
 		private webAuthnService: WebAuthnService,
+		private captchaService: CaptchaService,
 	) {
 	}
 
@@ -56,6 +63,10 @@ export class SigninApiService {
 				password: string;
 				token?: string;
 				credential?: AuthenticationResponseJSON;
+				'hcaptcha-response'?: string;
+				'g-recaptcha-response'?: string;
+				'turnstile-response'?: string;
+				'm-captcha-response'?: string;
 			};
 		}>,
 		reply: FastifyReply,
@@ -139,6 +150,32 @@ export class SigninApiService {
 		};
 
 		if (!profile.twoFactorEnabled) {
+			if (process.env.NODE_ENV !== 'test') {
+				if (this.meta.enableHcaptcha && this.meta.hcaptchaSecretKey) {
+					await this.captchaService.verifyHcaptcha(this.meta.hcaptchaSecretKey, body['hcaptcha-response']).catch(err => {
+						throw new FastifyReplyError(400, err);
+					});
+				}
+
+				if (this.meta.enableMcaptcha && this.meta.mcaptchaSecretKey && this.meta.mcaptchaSitekey && this.meta.mcaptchaInstanceUrl) {
+					await this.captchaService.verifyMcaptcha(this.meta.mcaptchaSecretKey, this.meta.mcaptchaSitekey, this.meta.mcaptchaInstanceUrl, body['m-captcha-response']).catch(err => {
+						throw new FastifyReplyError(400, err);
+					});
+				}
+
+				if (this.meta.enableRecaptcha && this.meta.recaptchaSecretKey) {
+					await this.captchaService.verifyRecaptcha(this.meta.recaptchaSecretKey, body['g-recaptcha-response']).catch(err => {
+						throw new FastifyReplyError(400, err);
+					});
+				}
+
+				if (this.meta.enableTurnstile && this.meta.turnstileSecretKey) {
+					await this.captchaService.verifyTurnstile(this.meta.turnstileSecretKey, body['turnstile-response']).catch(err => {
+						throw new FastifyReplyError(400, err);
+					});
+				}
+			}
+
 			if (same) {
 				return this.signinService.signin(request, reply, user);
 			} else {
diff --git a/packages/frontend/src/components/MkSignin.vue b/packages/frontend/src/components/MkSignin.vue
index 7942a84d66..8ebdac0220 100644
--- a/packages/frontend/src/components/MkSignin.vue
+++ b/packages/frontend/src/components/MkSignin.vue
@@ -32,7 +32,11 @@ SPDX-License-Identifier: AGPL-3.0-only
 				<template #prefix><i class="ti ti-lock"></i></template>
 				<template #caption><button class="_textButton" type="button" @click="resetPassword">{{ i18n.ts.forgotPassword }}</button></template>
 			</MkInput>
-			<MkButton type="submit" large primary rounded :disabled="signing" style="margin: 0 auto;">{{ signing ? i18n.ts.loggingIn : i18n.ts.login }}</MkButton>
+			<MkCaptcha v-if="instance.enableHcaptcha" ref="hcaptcha" v-model="hCaptchaResponse" :class="$style.captcha" provider="hcaptcha" :sitekey="instance.hcaptchaSiteKey"/>
+			<MkCaptcha v-if="instance.enableMcaptcha" ref="mcaptcha" v-model="mCaptchaResponse" :class="$style.captcha" provider="mcaptcha" :sitekey="instance.mcaptchaSiteKey" :instanceUrl="instance.mcaptchaInstanceUrl"/>
+			<MkCaptcha v-if="instance.enableRecaptcha" ref="recaptcha" v-model="reCaptchaResponse" :class="$style.captcha" provider="recaptcha" :sitekey="instance.recaptchaSiteKey"/>
+			<MkCaptcha v-if="instance.enableTurnstile" ref="turnstile" v-model="turnstileResponse" :class="$style.captcha" provider="turnstile" :sitekey="instance.turnstileSiteKey"/>
+			<MkButton type="submit" large primary rounded :disabled="captchaFailed || signing" style="margin: 0 auto;">{{ signing ? i18n.ts.loggingIn : i18n.ts.login }}</MkButton>
 		</div>
 		<div v-if="totpLogin" class="2fa-signin" :class="{ securityKeys: user && user.securityKeys }">
 			<div v-if="user && user.securityKeys" class="twofa-group tap-group">
@@ -68,7 +72,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { defineAsyncComponent, ref } from 'vue';
+import { computed, defineAsyncComponent, ref } from 'vue';
 import { toUnicode } from 'punycode/';
 import * as Misskey from 'misskey-js';
 import { supported as webAuthnSupported, get as webAuthnRequest, parseRequestOptionsFromJSON } from '@github/webauthn-json/browser-ponyfill';
@@ -85,6 +89,8 @@ import * as os from '@/os.js';
 import { misskeyApi } from '@/scripts/misskey-api.js';
 import { login } from '@/account.js';
 import { i18n } from '@/i18n.js';
+import { instance } from '@/instance.js';
+import MkCaptcha, { type Captcha } from '@/components/MkCaptcha.vue';
 
 const signing = ref(false);
 const user = ref<Misskey.entities.UserDetailed | null>(null);
@@ -98,6 +104,22 @@ const isBackupCode = ref(false);
 const queryingKey = ref(false);
 let credentialRequest: CredentialRequestOptions | null = null;
 const passkey_context = ref('');
+const hcaptcha = ref<Captcha | undefined>();
+const mcaptcha = ref<Captcha | undefined>();
+const recaptcha = ref<Captcha | undefined>();
+const turnstile = ref<Captcha | undefined>();
+const hCaptchaResponse = ref<string | null>(null);
+const mCaptchaResponse = ref<string | null>(null);
+const reCaptchaResponse = ref<string | null>(null);
+const turnstileResponse = ref<string | null>(null);
+
+const captchaFailed = computed((): boolean => {
+	return (
+		instance.enableHcaptcha && !hCaptchaResponse.value ||
+		instance.enableMcaptcha && !mCaptchaResponse.value ||
+		instance.enableRecaptcha && !reCaptchaResponse.value ||
+		instance.enableTurnstile && !turnstileResponse.value);
+});
 
 const emit = defineEmits<{
 	(ev: 'login', v: any): void;
@@ -227,6 +249,10 @@ function onSubmit(): void {
 		misskeyApi('signin', {
 			username: username.value,
 			password: password.value,
+			'hcaptcha-response': hCaptchaResponse.value,
+			'm-captcha-response': mCaptchaResponse.value,
+			'g-recaptcha-response': reCaptchaResponse.value,
+			'turnstile-response': turnstileResponse.value,
 			token: user.value?.twoFactorEnabled ? token.value : undefined,
 		}).then(res => {
 			emit('login', res);
@@ -236,6 +262,11 @@ function onSubmit(): void {
 }
 
 function loginFailed(err: any): void {
+	hcaptcha.value?.reset?.();
+	mcaptcha.value?.reset?.();
+	recaptcha.value?.reset?.();
+	turnstile.value?.reset?.();
+
 	switch (err.id) {
 		case '6cc579cc-885d-43d8-95c2-b8c7fc963280': {
 			os.alert({
diff --git a/packages/frontend/src/components/MkSignupDialog.form.vue b/packages/frontend/src/components/MkSignupDialog.form.vue
index 4ab4380ad5..38cac7f644 100644
--- a/packages/frontend/src/components/MkSignupDialog.form.vue
+++ b/packages/frontend/src/components/MkSignupDialog.form.vue
@@ -81,10 +81,10 @@ SPDX-License-Identifier: AGPL-3.0-only
 import { ref, computed } from 'vue';
 import { toUnicode } from 'punycode/';
 import * as Misskey from 'misskey-js';
+import * as config from '@@/js/config.js';
 import MkButton from './MkButton.vue';
 import MkInput from './MkInput.vue';
 import MkCaptcha, { type Captcha } from '@/components/MkCaptcha.vue';
-import * as config from '@@/js/config.js';
 import * as os from '@/os.js';
 import { misskeyApi } from '@/scripts/misskey-api.js';
 import { login } from '@/account.js';
@@ -105,6 +105,7 @@ const emit = defineEmits<{
 const host = toUnicode(config.host);
 
 const hcaptcha = ref<Captcha | undefined>();
+const mcaptcha = ref<Captcha | undefined>();
 const recaptcha = ref<Captcha | undefined>();
 const turnstile = ref<Captcha | undefined>();
 
@@ -281,6 +282,7 @@ async function onSubmit(): Promise<void> {
 	} catch {
 		submitting.value = false;
 		hcaptcha.value?.reset?.();
+		mcaptcha.value?.reset?.();
 		recaptcha.value?.reset?.();
 		turnstile.value?.reset?.();
 

From d3e2b59f537522168b0394d6e166b9ac5d166123 Mon Sep 17 00:00:00 2001
From: syuilo <4439005+syuilo@users.noreply.github.com>
Date: Thu, 3 Oct 2024 15:04:53 +0900
Subject: [PATCH 009/121] update deps

---
 packages/backend/package.json  |  24 +-
 packages/frontend/package.json |   2 +-
 pnpm-lock.yaml                 | 660 ++++++++++++++++-----------------
 3 files changed, 343 insertions(+), 343 deletions(-)

diff --git a/packages/backend/package.json b/packages/backend/package.json
index 6eed6fc725..bd5dab618a 100644
--- a/packages/backend/package.json
+++ b/packages/backend/package.json
@@ -71,20 +71,20 @@
 		"@bull-board/fastify": "6.0.0",
 		"@bull-board/ui": "6.0.0",
 		"@discordapp/twemoji": "15.1.0",
-		"@fastify/accepts": "5.0.0",
-		"@fastify/cookie": "10.0.0",
-		"@fastify/cors": "10.0.0",
-		"@fastify/express": "4.0.0",
+		"@fastify/accepts": "5.0.1",
+		"@fastify/cookie": "10.0.1",
+		"@fastify/cors": "10.0.1",
+		"@fastify/express": "4.0.1",
 		"@fastify/http-proxy": "10.0.0",
-		"@fastify/multipart": "9.0.0",
-		"@fastify/static": "8.0.0",
-		"@fastify/view": "10.0.0",
+		"@fastify/multipart": "9.0.1",
+		"@fastify/static": "8.0.1",
+		"@fastify/view": "10.0.1",
 		"@misskey-dev/sharp-read-bmp": "1.2.0",
 		"@misskey-dev/summaly": "5.1.0",
 		"@napi-rs/canvas": "0.1.56",
-		"@nestjs/common": "10.4.3",
-		"@nestjs/core": "10.4.3",
-		"@nestjs/testing": "10.4.3",
+		"@nestjs/common": "10.4.4",
+		"@nestjs/core": "10.4.4",
+		"@nestjs/testing": "10.4.4",
 		"@peertube/http-signature": "1.7.0",
 		"@sentry/node": "8.20.0",
 		"@sentry/profiling-node": "8.20.0",
@@ -149,7 +149,7 @@
 		"oauth2orize": "1.12.0",
 		"oauth2orize-pkce": "0.1.2",
 		"os-utils": "0.0.14",
-		"otpauth": "9.3.2",
+		"otpauth": "9.3.4",
 		"parse5": "7.1.2",
 		"pg": "8.13.0",
 		"pkce-challenge": "4.1.0",
@@ -187,7 +187,7 @@
 	},
 	"devDependencies": {
 		"@jest/globals": "29.7.0",
-		"@nestjs/platform-express": "10.4.3",
+		"@nestjs/platform-express": "10.4.4",
 		"@simplewebauthn/types": "10.0.0",
 		"@swc/jest": "0.2.36",
 		"@types/accepts": "1.3.7",
diff --git a/packages/frontend/package.json b/packages/frontend/package.json
index d3909babfd..02878c64d9 100644
--- a/packages/frontend/package.json
+++ b/packages/frontend/package.json
@@ -60,7 +60,7 @@
 		"rollup": "4.22.5",
 		"sanitize-html": "2.13.0",
 		"sass": "1.79.3",
-		"shiki": "1.12.0",
+		"shiki": "1.21.0",
 		"strict-event-emitter-types": "2.0.0",
 		"textarea-caret": "3.1.0",
 		"three": "0.169.0",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 0822620bf3..5d7febbcc9 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -102,29 +102,29 @@ importers:
         specifier: 15.1.0
         version: 15.1.0
       '@fastify/accepts':
-        specifier: 5.0.0
-        version: 5.0.0
+        specifier: 5.0.1
+        version: 5.0.1
       '@fastify/cookie':
-        specifier: 10.0.0
-        version: 10.0.0
+        specifier: 10.0.1
+        version: 10.0.1
       '@fastify/cors':
-        specifier: 10.0.0
-        version: 10.0.0
+        specifier: 10.0.1
+        version: 10.0.1
       '@fastify/express':
-        specifier: 4.0.0
-        version: 4.0.0
+        specifier: 4.0.1
+        version: 4.0.1
       '@fastify/http-proxy':
         specifier: 10.0.0
         version: 10.0.0(bufferutil@4.0.7)(utf-8-validate@6.0.3)
       '@fastify/multipart':
-        specifier: 9.0.0
-        version: 9.0.0
+        specifier: 9.0.1
+        version: 9.0.1
       '@fastify/static':
-        specifier: 8.0.0
-        version: 8.0.0
+        specifier: 8.0.1
+        version: 8.0.1
       '@fastify/view':
-        specifier: 10.0.0
-        version: 10.0.0
+        specifier: 10.0.1
+        version: 10.0.1
       '@misskey-dev/sharp-read-bmp':
         specifier: 1.2.0
         version: 1.2.0
@@ -135,14 +135,14 @@ importers:
         specifier: 0.1.56
         version: 0.1.56
       '@nestjs/common':
-        specifier: 10.4.3
-        version: 10.4.3(reflect-metadata@0.2.2)(rxjs@7.8.1)
+        specifier: 10.4.4
+        version: 10.4.4(reflect-metadata@0.2.2)(rxjs@7.8.1)
       '@nestjs/core':
-        specifier: 10.4.3
-        version: 10.4.3(@nestjs/common@10.4.3(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.3)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.1)
+        specifier: 10.4.4
+        version: 10.4.4(@nestjs/common@10.4.4(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.4)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.1)
       '@nestjs/testing':
-        specifier: 10.4.3
-        version: 10.4.3(@nestjs/common@10.4.3(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.3(@nestjs/common@10.4.3(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.3)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.3(@nestjs/common@10.4.3(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.3))
+        specifier: 10.4.4
+        version: 10.4.4(@nestjs/common@10.4.4(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.4(@nestjs/common@10.4.4(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.4)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.4(@nestjs/common@10.4.4(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.4))
       '@peertube/http-signature':
         specifier: 1.7.0
         version: 1.7.0
@@ -336,8 +336,8 @@ importers:
         specifier: 0.0.14
         version: 0.0.14
       otpauth:
-        specifier: 9.3.2
-        version: 9.3.2
+        specifier: 9.3.4
+        version: 9.3.4
       parse5:
         specifier: 7.1.2
         version: 7.1.2
@@ -533,8 +533,8 @@ importers:
         specifier: 29.7.0
         version: 29.7.0
       '@nestjs/platform-express':
-        specifier: 10.4.3
-        version: 10.4.3(@nestjs/common@10.4.3(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.3)
+        specifier: 10.4.4
+        version: 10.4.4(@nestjs/common@10.4.4(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.4)
       '@simplewebauthn/types':
         specifier: 10.0.0
         version: 10.0.0
@@ -821,8 +821,8 @@ importers:
         specifier: 1.79.3
         version: 1.79.3
       shiki:
-        specifier: 1.12.0
-        version: 1.12.0
+        specifier: 1.21.0
+        version: 1.21.0
       strict-event-emitter-types:
         specifier: 2.0.0
         version: 2.0.0
@@ -2710,8 +2710,8 @@ packages:
   '@fastify/accept-negotiator@2.0.0':
     resolution: {integrity: sha512-/Sce/kBzuTxIq5tJh85nVNOq9wKD8s+viIgX0fFMDBdw95gnpf53qmF1oBgJym3cPFliWUuSloVg/1w/rH0FcQ==}
 
-  '@fastify/accepts@5.0.0':
-    resolution: {integrity: sha512-5wpgycrn+DXPkATGqUbXY9tyqLNgxo9S8f0EHUyIWvUacor2cXa3liYZggsqoyMXgpIqUbGLPBl+dN2hRcU9jQ==}
+  '@fastify/accepts@5.0.1':
+    resolution: {integrity: sha512-8ji2MGTbceSnAXKYx/U9iWt6Fmf0zJovh0meO5rpwYS/vy0Z3QIR2J/hKmbcTpYfMu5NUliNpsAtMavmzBQhmA==}
 
   '@fastify/ajv-compiler@4.0.0':
     resolution: {integrity: sha512-dt0jyLAlay14LpIn4Fg1SY7V5NJ9KH0YFDpYVQY5cgIVBvdI8908AMx5zQ0bBYPGT6Wh+bM3f2caMmOXLP3QsQ==}
@@ -2723,11 +2723,11 @@ packages:
   '@fastify/busboy@3.0.0':
     resolution: {integrity: sha512-83rnH2nCvclWaPQQKvkJ2pdOjG4TZyEVuFDnlOF6KP08lDaaceVyw/W63mDuafQT+MKHCvXIPpE5uYWeM0rT4w==}
 
-  '@fastify/cookie@10.0.0':
-    resolution: {integrity: sha512-S43spazwAfzm5nKlqq/spAGW+O6r+WQzg5vXXI1ArCXXFa8KBA/tiU3XRVQUehSNtbN5PA6+g183hzh5/dZ6Iw==}
+  '@fastify/cookie@10.0.1':
+    resolution: {integrity: sha512-NV/wbCUv4ETJ5KM1KMu0fLx0nSCm9idIxwg66NZnNbfPQH3rdbx6k0qRs5uy0y+MhBgvDudYRA30KlK659chyw==}
 
-  '@fastify/cors@10.0.0':
-    resolution: {integrity: sha512-kb9fkc/LVbLTQ3lhA+ZZjC/Styzysodo/MTCdVCvTtgHa/gBwxrEEkcp3fuoKIfAQt85wksrpXjUGbw5NQffEQ==}
+  '@fastify/cors@10.0.1':
+    resolution: {integrity: sha512-O8JIf6448uQbOgzSkCqhClw6gFTAqrdfeA6R3fc/3gwTJGUp7gl8/3tbNB+6INuu4RmgVOq99BmvdGbtu5pgOA==}
 
   '@fastify/deepmerge@2.0.0':
     resolution: {integrity: sha512-fsaybTGDyQ5KpPsplQqb9yKdCf2x/pbNpMNk8Tvp3rRz7lVcupKysH4b2ELMN2P4Hak1+UqTYdTj/u4FNV2p0g==}
@@ -2735,8 +2735,8 @@ packages:
   '@fastify/error@4.0.0':
     resolution: {integrity: sha512-OO/SA8As24JtT1usTUTKgGH7uLvhfwZPwlptRi2Dp5P4KKmJI3gvsZ8MIHnNwDs4sLf/aai5LzTyl66xr7qMxA==}
 
-  '@fastify/express@4.0.0':
-    resolution: {integrity: sha512-e+IMKKV9+HRCVm7LVW8PaMrpEerHfqNLpRkbiVHYfVm0xeOphiwyNEoge4VA3Sh8gubtDfo9yKkpRzx6gx63kg==}
+  '@fastify/express@4.0.1':
+    resolution: {integrity: sha512-mEQ6pawaENeZ3swqVtkxdLi8NQC5eKBkclE+7ma1qQMuB+yI6WxDyEp55pdbqPIqBQTN/cGgHv84qxVS7NKC2Q==}
 
   '@fastify/fast-json-stringify-compiler@5.0.0':
     resolution: {integrity: sha512-tywfuZfXsyxLC5kEqrMubbFa9vpAxNtuPE7j9w5si1r+6p5b981pDfZ5Y8HBqmjDQl+PABT7cV5jZgXI2j+I5g==}
@@ -2747,8 +2747,8 @@ packages:
   '@fastify/merge-json-schemas@0.1.1':
     resolution: {integrity: sha512-fERDVz7topgNjtXsJTTW1JKLy0rhuLRcquYqNR9rF7OcVpCa2OVW49ZPDIhaRRCaUuvVxI+N416xUoF76HNSXA==}
 
-  '@fastify/multipart@9.0.0':
-    resolution: {integrity: sha512-B/rzOl1wmkj4LddH2i+zR8Gke8ZX1J8D7n4uJeis5VdIa7OR9Ys/TzUxI0/h1SF9ubHlNhBP+eO/FwnftarP9w==}
+  '@fastify/multipart@9.0.1':
+    resolution: {integrity: sha512-vt2gOCw/O4EwpN4KlLVJxth4iQlDf7T5ggw2Db2C+UbO2WJBG7y0jEBvu/HT6JIW/lBYaqrrUy9MmTpCKgXEpw==}
 
   '@fastify/reply-from@11.0.0':
     resolution: {integrity: sha512-dv3o8hyy4sxhg1RN9l6ueM+PMMaIPKLjtL2T99H5M7h1Xt8d1RX3r+xC+sL5AqJqLvReX4N+7mTq9QDeB8i6Lg==}
@@ -2756,15 +2756,9 @@ packages:
   '@fastify/send@3.1.1':
     resolution: {integrity: sha512-LdiV2mle/2tH8vh6GwGl0ubfUAgvY+9yF9oGI1iiwVyNUVOQamvw5n+OFu6iCNNoyuCY80FFURBn4TZCbTe8LA==}
 
-  '@fastify/static@8.0.0':
-    resolution: {integrity: sha512-VKGn1PQslB2VqzspyMKPu9xasF9vj+YuyGhVLb1ih6V60VVcRvcf0fFRcl3opt6c6YWwhKKdTUTfVE6COnpw6A==}
-
   '@fastify/static@8.0.1':
     resolution: {integrity: sha512-7idyhbcgf14v4bjWzUeHEFvnVxvNJ1n5cyGPgFtwTZjnjUQ1wgC7a2FQai7OGKqCKywDEjzbPhAZRW+uEK1LMg==}
 
-  '@fastify/view@10.0.0':
-    resolution: {integrity: sha512-2KnfgpSbAImKV5kKdNAkSyjV+9kYUYLvgDLx/wlzgqel92bN9Z520cwG3g3bAkr0yVnEJu62dIm2qAL9FASS1w==}
-
   '@fastify/view@10.0.1':
     resolution: {integrity: sha512-rXtBN0oVDmoRZAS7lelrCIahf+qFtlMOOas8VPdA7JvrJ9ChcF7e36pIUPU0Vbs3KmHxESUb7XatavUZEe/k5Q==}
 
@@ -3223,8 +3217,8 @@ packages:
     resolution: {integrity: sha512-SujSchzG6lLc/wT+Mwxam/w30Kk2sFTiU6bLFcidecKSmlhenAhGMQhZh2iGFfKoh2+8iit0jrt99n6TqReICQ==}
     engines: {node: '>= 10'}
 
-  '@nestjs/common@10.4.3':
-    resolution: {integrity: sha512-4hbLd3XIJubHSylYd/1WSi4VQvG68KM/ECYpMDqA3k3J1/T17SAg40sDoq3ZoO5OZgU0xuNyjuISdOTjs11qVg==}
+  '@nestjs/common@10.4.4':
+    resolution: {integrity: sha512-0j2/zqRw9nvHV1GKTktER8B/hIC/Z8CYFjN/ZqUuvwayCH+jZZBhCR2oRyuvLTXdnlSmtCAg2xvQ0ULqQvzqhA==}
     peerDependencies:
       class-transformer: '*'
       class-validator: '*'
@@ -3236,8 +3230,8 @@ packages:
       class-validator:
         optional: true
 
-  '@nestjs/core@10.4.3':
-    resolution: {integrity: sha512-6OQz+5C8mT8yRtfvE5pPCq+p6w5jDot+oQku1KzQ24ABn+lay1KGuJwcKZhdVNuselx+8xhdMxknZTA8wrGLIg==}
+  '@nestjs/core@10.4.4':
+    resolution: {integrity: sha512-y9tjmAzU6LTh1cC/lWrRsCcOd80khSR0qAHAqwY2svbW+AhsR/XCzgpZrAAKJrm/dDfjLCZKyxJSayeirGcW5Q==}
     peerDependencies:
       '@nestjs/common': ^10.0.0
       '@nestjs/microservices': ^10.0.0
@@ -3253,14 +3247,14 @@ packages:
       '@nestjs/websockets':
         optional: true
 
-  '@nestjs/platform-express@10.4.3':
-    resolution: {integrity: sha512-ss7gkofVm3eO+1P9iRhmGq6Xcjg+mIN3dWisKJZYelSV+msb0QpJmqChLvWjLkWtlqDnx915FKUk0IzCa0TVzw==}
+  '@nestjs/platform-express@10.4.4':
+    resolution: {integrity: sha512-y52q1MxhbHaT3vAgWd08RgiYon0lJgtTa8U6g6gV0KI0IygwZhDQFJVxnrRDUdxQGIP5CKHmfQu3sk9gTNFoEA==}
     peerDependencies:
       '@nestjs/common': ^10.0.0
       '@nestjs/core': ^10.0.0
 
-  '@nestjs/testing@10.4.3':
-    resolution: {integrity: sha512-SBNWrMU51YAlYmW86wyjlGZ2uLnASNiOPD0lBcNIlxxei0b05/aI3nh7OPuxbXQUdedUJfPq2d2jZj4TRG4S0w==}
+  '@nestjs/testing@10.4.4':
+    resolution: {integrity: sha512-qRGFj51A5RM7JqA8pcyEwSLA3Y0dle/PAZ8oxP0suimoCusRY3Tk7wYqutZdCNj1ATb678SDaUZDHk2pwSv9/g==}
     peerDependencies:
       '@nestjs/common': ^10.0.0
       '@nestjs/core': ^10.0.0
@@ -3272,9 +3266,9 @@ packages:
       '@nestjs/platform-express':
         optional: true
 
-  '@noble/hashes@1.4.0':
-    resolution: {integrity: sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==}
-    engines: {node: '>= 16'}
+  '@noble/hashes@1.5.0':
+    resolution: {integrity: sha512-1j6kQFb7QRru7eKN3ZDvRcP13rugwdxZqCjbiAVZfIJwgj2A65UmT4TgARXGlXgnRkORLTDTrO19ZErt7+QXgA==}
+    engines: {node: ^14.21.3 || >=16}
 
   '@nodelib/fs.scandir@2.1.5':
     resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
@@ -3703,6 +3697,21 @@ packages:
   '@shikijs/core@1.12.0':
     resolution: {integrity: sha512-mc1cLbm6UQ8RxLc0dZES7v5rkH+99LxQp/ZvTqV3NLyYsO/fD6JhEflP1H5b2SDq9gI0+0G36AVZWxvounfR9w==}
 
+  '@shikijs/core@1.21.0':
+    resolution: {integrity: sha512-zAPMJdiGuqXpZQ+pWNezQAk5xhzRXBNiECFPcJLtUdsFM3f//G95Z15EHTnHchYycU8kIIysqGgxp8OVSj1SPQ==}
+
+  '@shikijs/engine-javascript@1.21.0':
+    resolution: {integrity: sha512-jxQHNtVP17edFW4/0vICqAVLDAxmyV31MQJL4U/Kg+heQALeKYVOWo0sMmEZ18FqBt+9UCdyqGKYE7bLRtk9mg==}
+
+  '@shikijs/engine-oniguruma@1.21.0':
+    resolution: {integrity: sha512-AIZ76XocENCrtYzVU7S4GY/HL+tgHGbVU+qhiDyNw1qgCA5OSi4B4+HY4BtAoJSMGuD/L5hfTzoRVbzEm2WTvg==}
+
+  '@shikijs/types@1.21.0':
+    resolution: {integrity: sha512-tzndANDhi5DUndBtpojEq/42+dpUF2wS7wdCDQaFtIXm3Rd1QkrcVgSSRLOvEwexekihOXfbYJINW37g96tJRw==}
+
+  '@shikijs/vscode-textmate@9.2.2':
+    resolution: {integrity: sha512-TMp15K+GGYrWlZM8+Lnj9EaHEFmOen0WJBrfa17hF7taDOYthuPPV0GWzfd/9iMij0akS/8Yw2ikquH7uVi/fg==}
+
   '@sideway/address@4.1.4':
     resolution: {integrity: sha512-7vwq+rOHVWjyXxVlR76Agnvhy8I9rpzjosTESvmhNeXOXdZZB15Fl+TI9x1SiHZH5Jv2wTGduSxFDIaq0m3DUw==}
 
@@ -5580,10 +5589,6 @@ packages:
   bn.js@4.12.0:
     resolution: {integrity: sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==}
 
-  body-parser@1.20.2:
-    resolution: {integrity: sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==}
-    engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16}
-
   body-parser@1.20.3:
     resolution: {integrity: sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==}
     engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16}
@@ -5786,6 +5791,12 @@ packages:
     resolution: {integrity: sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==}
     engines: {node: '>=10'}
 
+  character-entities-html4@2.1.0:
+    resolution: {integrity: sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==}
+
+  character-entities-legacy@3.0.0:
+    resolution: {integrity: sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==}
+
   character-entities@2.0.2:
     resolution: {integrity: sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==}
 
@@ -5964,6 +5975,9 @@ packages:
     resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
     engines: {node: '>= 0.8'}
 
+  comma-separated-tokens@2.0.3:
+    resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==}
+
   commander@10.0.1:
     resolution: {integrity: sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==}
     engines: {node: '>=14'}
@@ -6821,10 +6835,6 @@ packages:
   exponential-backoff@3.1.1:
     resolution: {integrity: sha512-dX7e/LHVJ6W3DE1MHWi9S1EYzDESENfLrYohG2G++ovZrYOkm4Knwa0mc1cn84xJOR4KEU0WSchhLbd0UklbHw==}
 
-  express@4.19.2:
-    resolution: {integrity: sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==}
-    engines: {node: '>= 0.10.0'}
-
   express@4.21.0:
     resolution: {integrity: sha512-VqcNGcj/Id5ZT1LZ/cfihi3ttTn+NJmkli2eZADigjq29qTlWi/hAQ43t/VLPq8+UX06FCEx3ByOYet6ZFblng==}
     engines: {node: '>= 0.10.0'}
@@ -6967,10 +6977,6 @@ packages:
     resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==}
     engines: {node: '>=8'}
 
-  finalhandler@1.2.0:
-    resolution: {integrity: sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==}
-    engines: {node: '>= 0.8'}
-
   finalhandler@1.3.1:
     resolution: {integrity: sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==}
     engines: {node: '>= 0.8'}
@@ -7372,9 +7378,15 @@ packages:
   hast-util-is-element@3.0.0:
     resolution: {integrity: sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g==}
 
+  hast-util-to-html@9.0.3:
+    resolution: {integrity: sha512-M17uBDzMJ9RPCqLMO92gNNUDuBSq10a25SDBI08iCCxmorf4Yy6sYHK57n9WAbRAAaU+DuR4W6GN9K4DFZesYg==}
+
   hast-util-to-string@3.0.0:
     resolution: {integrity: sha512-OGkAxX1Ua3cbcW6EJ5pT/tslVb90uViVkcJ4ZZIMW/R33DX/AkcJcRrPebPwJkHYwlDHXz4aIwvAAaAdtrACFA==}
 
+  hast-util-whitespace@3.0.0:
+    resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==}
+
   he@1.2.0:
     resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==}
     hasBin: true
@@ -7414,6 +7426,9 @@ packages:
     resolution: {integrity: sha512-vy7ClnArOZwCnqZgvv+ddgHgJiAFXe3Ge9ML5/mBctVJoUoYPCdxVucOywjDARn6CVoh3dRSFdPHy2sX80L0Wg==}
     engines: {node: '>=8'}
 
+  html-void-elements@3.0.0:
+    resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==}
+
   htmlescape@1.1.1:
     resolution: {integrity: sha512-eVcrzgbR4tim7c7soKQKtxa/kQM4TzjnlU83rcZ9bHU6t31ehfV7SktN6McWgwPWg+JYMA/O3qpGxBvFq1z2Jg==}
     engines: {node: '>=0.10'}
@@ -8437,6 +8452,9 @@ packages:
   mdast-util-phrasing@4.1.0:
     resolution: {integrity: sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==}
 
+  mdast-util-to-hast@13.2.0:
+    resolution: {integrity: sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA==}
+
   mdast-util-to-markdown@2.1.0:
     resolution: {integrity: sha512-SR2VnIEdVNCJbP6y7kVTJgPLifdr8WEU440fQec7qHoHOUz/oJ2jmNRqdDQ3rbiStOXb2mCDGTuwsK5OPUgYlQ==}
 
@@ -8466,9 +8484,6 @@ packages:
     resolution: {integrity: sha512-+obSblOQmRhcyBt62furQqRAQpNyWXo8BuQ5bN7dG8wmwQ+vwHKp/rCFD4CrTP8CsDQD1sjoZ94K417XEUk8IQ==}
     engines: {node: '>=10'}
 
-  merge-descriptors@1.0.1:
-    resolution: {integrity: sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==}
-
   merge-descriptors@1.0.3:
     resolution: {integrity: sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==}
 
@@ -9088,6 +9103,9 @@ packages:
     resolution: {integrity: sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==}
     engines: {node: '>=12'}
 
+  oniguruma-to-js@0.4.3:
+    resolution: {integrity: sha512-X0jWUcAlxORhOqqBREgPMgnshB7ZGYszBNspP+tS9hPD3l13CdaXcHbgImoHUHlrvGx/7AvFEkTRhAGYh+jzjQ==}
+
   open@8.4.2:
     resolution: {integrity: sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==}
     engines: {node: '>=12'}
@@ -9119,8 +9137,8 @@ packages:
   ospath@1.2.2:
     resolution: {integrity: sha512-o6E5qJV5zkAbIDNhGSIlyOhScKXgQrSRMilfph0clDfM0nEnBOlKlH4sWDmG95BW/CvwNz0vmm7dJVtU2KlMiA==}
 
-  otpauth@9.3.2:
-    resolution: {integrity: sha512-KixtXWN9RGdS8WHPfDo7qsOYiivCbl+VeLBT+7HBTtJebBO6aXr/bpZXr+TwY2COecdY82VeBghm31mLYQVZlQ==}
+  otpauth@9.3.4:
+    resolution: {integrity: sha512-qXv+lpsCUO9ewitLYfeDKbLYt7UUCivnU/fwGK2OqhgrCBsRkTUNKWsgKAhkXG3aistOY+jEeuL90JEBu6W3mQ==}
 
   outvariant@1.4.2:
     resolution: {integrity: sha512-Ou3dJ6bA/UJ5GVHxah4LnqDwZRwAmWxrG3wtrHrbGnP4RnLCtA64A4F+ae7Y8ww660JaddSoArUR5HjipWSHAQ==}
@@ -9260,9 +9278,6 @@ packages:
   path-to-regexp@0.1.10:
     resolution: {integrity: sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==}
 
-  path-to-regexp@0.1.7:
-    resolution: {integrity: sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==}
-
   path-to-regexp@1.8.0:
     resolution: {integrity: sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==}
 
@@ -9719,6 +9734,9 @@ packages:
   prop-types@15.8.1:
     resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==}
 
+  property-information@6.5.0:
+    resolution: {integrity: sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig==}
+
   proto-list@1.2.4:
     resolution: {integrity: sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==}
 
@@ -9811,10 +9829,6 @@ packages:
     engines: {node: '>=10.13.0'}
     hasBin: true
 
-  qs@6.11.0:
-    resolution: {integrity: sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==}
-    engines: {node: '>=0.6'}
-
   qs@6.13.0:
     resolution: {integrity: sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==}
     engines: {node: '>=0.6'}
@@ -9986,6 +10000,9 @@ packages:
   regenerator-runtime@0.14.0:
     resolution: {integrity: sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==}
 
+  regex@4.3.3:
+    resolution: {integrity: sha512-r/AadFO7owAq1QJVeZ/nq9jNS1vyZt+6t1p/E59B56Rn2GCya+gr1KSyOzNL/er+r+B7phv5jG2xU2Nz1YkmJg==}
+
   regexp.prototype.flags@1.5.0:
     resolution: {integrity: sha512-0SutC3pNudRKgquxGoRGIz946MZVHqbNfPjBdxeOhBrdgDKlRoXmYLQN9xRbrR09ZXWeGAdPuif7egofn6v5LA==}
     engines: {node: '>= 0.4'}
@@ -10212,18 +10229,10 @@ packages:
     engines: {node: '>=10'}
     hasBin: true
 
-  send@0.18.0:
-    resolution: {integrity: sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==}
-    engines: {node: '>= 0.8.0'}
-
   send@0.19.0:
     resolution: {integrity: sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==}
     engines: {node: '>= 0.8.0'}
 
-  serve-static@1.15.0:
-    resolution: {integrity: sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==}
-    engines: {node: '>= 0.8.0'}
-
   serve-static@1.16.2:
     resolution: {integrity: sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==}
     engines: {node: '>= 0.8.0'}
@@ -10275,6 +10284,9 @@ packages:
   shiki@1.12.0:
     resolution: {integrity: sha512-BuAxWOm5JhRcbSOl7XCei8wGjgJJonnV0oipUupPY58iULxUGyHhW5CF+9FRMuM1pcJ5cGEJGll1LusX6FwpPA==}
 
+  shiki@1.21.0:
+    resolution: {integrity: sha512-apCH5BoWTrmHDPGgg3RF8+HAAbEL/CdbYr8rMw7eIrdhCkZHdVGat5mMNlRtd1erNG01VPMIKHNQ0Pj2HMAiog==}
+
   shimmer@1.2.1:
     resolution: {integrity: sha512-sQTKC1Re/rM6XyFM6fIAGHRPVGvyXfgzIDvzoq608vM+jeyVD0Tu1E6Np0Kc2zAIFWIj963V2800iF/9LPieQw==}
 
@@ -10614,6 +10626,9 @@ packages:
   string_decoder@1.3.0:
     resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==}
 
+  stringify-entities@4.0.4:
+    resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==}
+
   stringz@2.1.0:
     resolution: {integrity: sha512-KlywLT+MZ+v0IRepfMxRtnSvDCMc3nR1qqCs3m/qIbSOWkNZYT8XHQA31rS3TnKp0c5xjZu3M4GY/2aRKSi/6A==}
 
@@ -10866,6 +10881,9 @@ packages:
   trace-redirect@1.0.6:
     resolution: {integrity: sha512-UUfa1DjjU5flcjMdaFIiIEGDTyu2y/IiMjOX4uGXa7meKBS4vD4f2Uy/tken9Qkd4Jsm4sRsfZcIIPqrRVF3Mg==}
 
+  trim-lines@3.0.1:
+    resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==}
+
   trim-newlines@3.0.1:
     resolution: {integrity: sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw==}
     engines: {node: '>=8'}
@@ -11146,6 +11164,9 @@ packages:
   unist-util-is@6.0.0:
     resolution: {integrity: sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==}
 
+  unist-util-position@5.0.0:
+    resolution: {integrity: sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==}
+
   unist-util-stringify-position@4.0.0:
     resolution: {integrity: sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==}
 
@@ -11693,13 +11714,13 @@ snapshots:
     dependencies:
       '@aws-crypto/util': 5.2.0
       '@aws-sdk/types': 3.609.0
-      tslib: 2.6.3
+      tslib: 2.7.0
 
   '@aws-crypto/crc32c@5.2.0':
     dependencies:
       '@aws-crypto/util': 5.2.0
       '@aws-sdk/types': 3.609.0
-      tslib: 2.6.3
+      tslib: 2.7.0
 
   '@aws-crypto/sha1-browser@5.2.0':
     dependencies:
@@ -11708,7 +11729,7 @@ snapshots:
       '@aws-sdk/types': 3.609.0
       '@aws-sdk/util-locate-window': 3.208.0
       '@smithy/util-utf8': 2.0.0
-      tslib: 2.6.3
+      tslib: 2.7.0
 
   '@aws-crypto/sha256-browser@5.2.0':
     dependencies:
@@ -11718,23 +11739,23 @@ snapshots:
       '@aws-sdk/types': 3.609.0
       '@aws-sdk/util-locate-window': 3.208.0
       '@smithy/util-utf8': 2.0.0
-      tslib: 2.6.3
+      tslib: 2.7.0
 
   '@aws-crypto/sha256-js@5.2.0':
     dependencies:
       '@aws-crypto/util': 5.2.0
       '@aws-sdk/types': 3.609.0
-      tslib: 2.6.3
+      tslib: 2.7.0
 
   '@aws-crypto/supports-web-crypto@5.2.0':
     dependencies:
-      tslib: 2.6.3
+      tslib: 2.7.0
 
   '@aws-crypto/util@5.2.0':
     dependencies:
       '@aws-sdk/types': 3.609.0
       '@smithy/util-utf8': 2.0.0
-      tslib: 2.6.3
+      tslib: 2.7.0
 
   '@aws-sdk/client-s3@3.620.0':
     dependencies:
@@ -11840,7 +11861,7 @@ snapshots:
       '@smithy/util-middleware': 3.0.3
       '@smithy/util-retry': 3.0.3
       '@smithy/util-utf8': 3.0.0
-      tslib: 2.6.3
+      tslib: 2.7.0
     transitivePeerDependencies:
       - aws-crt
 
@@ -11883,7 +11904,7 @@ snapshots:
       '@smithy/util-middleware': 3.0.3
       '@smithy/util-retry': 3.0.3
       '@smithy/util-utf8': 3.0.0
-      tslib: 2.6.3
+      tslib: 2.7.0
     transitivePeerDependencies:
       - aws-crt
 
@@ -11928,7 +11949,7 @@ snapshots:
       '@smithy/util-middleware': 3.0.3
       '@smithy/util-retry': 3.0.3
       '@smithy/util-utf8': 3.0.0
-      tslib: 2.6.3
+      tslib: 2.7.0
     transitivePeerDependencies:
       - aws-crt
 
@@ -11940,14 +11961,14 @@ snapshots:
       '@smithy/smithy-client': 3.1.11
       '@smithy/types': 3.3.0
       fast-xml-parser: 4.2.5
-      tslib: 2.6.3
+      tslib: 2.7.0
 
   '@aws-sdk/credential-provider-env@3.609.0':
     dependencies:
       '@aws-sdk/types': 3.609.0
       '@smithy/property-provider': 3.1.3
       '@smithy/types': 3.3.0
-      tslib: 2.6.3
+      tslib: 2.7.0
 
   '@aws-sdk/credential-provider-http@3.620.0':
     dependencies:
@@ -11959,7 +11980,7 @@ snapshots:
       '@smithy/smithy-client': 3.1.11
       '@smithy/types': 3.3.0
       '@smithy/util-stream': 3.1.3
-      tslib: 2.6.3
+      tslib: 2.7.0
 
   '@aws-sdk/credential-provider-ini@3.620.0(@aws-sdk/client-sso-oidc@3.620.0(@aws-sdk/client-sts@3.620.0))(@aws-sdk/client-sts@3.620.0)':
     dependencies:
@@ -11974,7 +11995,7 @@ snapshots:
       '@smithy/property-provider': 3.1.3
       '@smithy/shared-ini-file-loader': 3.1.4
       '@smithy/types': 3.3.0
-      tslib: 2.6.3
+      tslib: 2.7.0
     transitivePeerDependencies:
       - '@aws-sdk/client-sso-oidc'
       - aws-crt
@@ -11992,7 +12013,7 @@ snapshots:
       '@smithy/property-provider': 3.1.3
       '@smithy/shared-ini-file-loader': 3.1.4
       '@smithy/types': 3.3.0
-      tslib: 2.6.3
+      tslib: 2.7.0
     transitivePeerDependencies:
       - '@aws-sdk/client-sso-oidc'
       - '@aws-sdk/client-sts'
@@ -12004,7 +12025,7 @@ snapshots:
       '@smithy/property-provider': 3.1.3
       '@smithy/shared-ini-file-loader': 3.1.4
       '@smithy/types': 3.3.0
-      tslib: 2.6.3
+      tslib: 2.7.0
 
   '@aws-sdk/credential-provider-sso@3.620.0(@aws-sdk/client-sso-oidc@3.620.0(@aws-sdk/client-sts@3.620.0))':
     dependencies:
@@ -12014,7 +12035,7 @@ snapshots:
       '@smithy/property-provider': 3.1.3
       '@smithy/shared-ini-file-loader': 3.1.4
       '@smithy/types': 3.3.0
-      tslib: 2.6.3
+      tslib: 2.7.0
     transitivePeerDependencies:
       - '@aws-sdk/client-sso-oidc'
       - aws-crt
@@ -12025,7 +12046,7 @@ snapshots:
       '@aws-sdk/types': 3.609.0
       '@smithy/property-provider': 3.1.3
       '@smithy/types': 3.3.0
-      tslib: 2.6.3
+      tslib: 2.7.0
 
   '@aws-sdk/lib-storage@3.620.0(@aws-sdk/client-s3@3.620.0)':
     dependencies:
@@ -12046,14 +12067,14 @@ snapshots:
       '@smithy/protocol-http': 4.1.0
       '@smithy/types': 3.3.0
       '@smithy/util-config-provider': 3.0.0
-      tslib: 2.6.3
+      tslib: 2.7.0
 
   '@aws-sdk/middleware-expect-continue@3.620.0':
     dependencies:
       '@aws-sdk/types': 3.609.0
       '@smithy/protocol-http': 4.1.0
       '@smithy/types': 3.3.0
-      tslib: 2.6.3
+      tslib: 2.7.0
 
   '@aws-sdk/middleware-flexible-checksums@3.620.0':
     dependencies:
@@ -12064,33 +12085,33 @@ snapshots:
       '@smithy/protocol-http': 4.1.0
       '@smithy/types': 3.3.0
       '@smithy/util-utf8': 3.0.0
-      tslib: 2.6.3
+      tslib: 2.7.0
 
   '@aws-sdk/middleware-host-header@3.620.0':
     dependencies:
       '@aws-sdk/types': 3.609.0
       '@smithy/protocol-http': 4.1.0
       '@smithy/types': 3.3.0
-      tslib: 2.6.3
+      tslib: 2.7.0
 
   '@aws-sdk/middleware-location-constraint@3.609.0':
     dependencies:
       '@aws-sdk/types': 3.609.0
       '@smithy/types': 3.3.0
-      tslib: 2.6.3
+      tslib: 2.7.0
 
   '@aws-sdk/middleware-logger@3.609.0':
     dependencies:
       '@aws-sdk/types': 3.609.0
       '@smithy/types': 3.3.0
-      tslib: 2.6.3
+      tslib: 2.7.0
 
   '@aws-sdk/middleware-recursion-detection@3.620.0':
     dependencies:
       '@aws-sdk/types': 3.609.0
       '@smithy/protocol-http': 4.1.0
       '@smithy/types': 3.3.0
-      tslib: 2.6.3
+      tslib: 2.7.0
 
   '@aws-sdk/middleware-sdk-s3@3.620.0':
     dependencies:
@@ -12104,7 +12125,7 @@ snapshots:
       '@smithy/util-config-provider': 3.0.0
       '@smithy/util-stream': 3.1.3
       '@smithy/util-utf8': 3.0.0
-      tslib: 2.6.3
+      tslib: 2.7.0
 
   '@aws-sdk/middleware-signing@3.620.0':
     dependencies:
@@ -12114,13 +12135,13 @@ snapshots:
       '@smithy/signature-v4': 4.1.0
       '@smithy/types': 3.3.0
       '@smithy/util-middleware': 3.0.3
-      tslib: 2.6.3
+      tslib: 2.7.0
 
   '@aws-sdk/middleware-ssec@3.609.0':
     dependencies:
       '@aws-sdk/types': 3.609.0
       '@smithy/types': 3.3.0
-      tslib: 2.6.3
+      tslib: 2.7.0
 
   '@aws-sdk/middleware-user-agent@3.620.0':
     dependencies:
@@ -12128,7 +12149,7 @@ snapshots:
       '@aws-sdk/util-endpoints': 3.614.0
       '@smithy/protocol-http': 4.1.0
       '@smithy/types': 3.3.0
-      tslib: 2.6.3
+      tslib: 2.7.0
 
   '@aws-sdk/region-config-resolver@3.614.0':
     dependencies:
@@ -12137,7 +12158,7 @@ snapshots:
       '@smithy/types': 3.3.0
       '@smithy/util-config-provider': 3.0.0
       '@smithy/util-middleware': 3.0.3
-      tslib: 2.6.3
+      tslib: 2.7.0
 
   '@aws-sdk/signature-v4-multi-region@3.620.0':
     dependencies:
@@ -12146,7 +12167,7 @@ snapshots:
       '@smithy/protocol-http': 4.1.0
       '@smithy/signature-v4': 4.1.0
       '@smithy/types': 3.3.0
-      tslib: 2.6.3
+      tslib: 2.7.0
 
   '@aws-sdk/token-providers@3.614.0(@aws-sdk/client-sso-oidc@3.620.0(@aws-sdk/client-sts@3.620.0))':
     dependencies:
@@ -12155,46 +12176,46 @@ snapshots:
       '@smithy/property-provider': 3.1.3
       '@smithy/shared-ini-file-loader': 3.1.4
       '@smithy/types': 3.3.0
-      tslib: 2.6.3
+      tslib: 2.7.0
 
   '@aws-sdk/types@3.609.0':
     dependencies:
       '@smithy/types': 3.3.0
-      tslib: 2.6.3
+      tslib: 2.7.0
 
   '@aws-sdk/util-arn-parser@3.568.0':
     dependencies:
-      tslib: 2.6.3
+      tslib: 2.7.0
 
   '@aws-sdk/util-endpoints@3.614.0':
     dependencies:
       '@aws-sdk/types': 3.609.0
       '@smithy/types': 3.3.0
       '@smithy/util-endpoints': 2.0.5
-      tslib: 2.6.3
+      tslib: 2.7.0
 
   '@aws-sdk/util-locate-window@3.208.0':
     dependencies:
-      tslib: 2.6.3
+      tslib: 2.7.0
 
   '@aws-sdk/util-user-agent-browser@3.609.0':
     dependencies:
       '@aws-sdk/types': 3.609.0
       '@smithy/types': 3.3.0
       bowser: 2.11.0
-      tslib: 2.6.3
+      tslib: 2.7.0
 
   '@aws-sdk/util-user-agent-node@3.614.0':
     dependencies:
       '@aws-sdk/types': 3.609.0
       '@smithy/node-config-provider': 3.1.4
       '@smithy/types': 3.3.0
-      tslib: 2.6.3
+      tslib: 2.7.0
 
   '@aws-sdk/xml-builder@3.609.0':
     dependencies:
       '@smithy/types': 3.3.0
-      tslib: 2.6.3
+      tslib: 2.7.0
 
   '@babel/code-frame@7.23.5':
     dependencies:
@@ -12223,7 +12244,7 @@ snapshots:
       '@babel/traverse': 7.23.5
       '@babel/types': 7.24.7
       convert-source-map: 2.0.0
-      debug: 4.3.5(supports-color@8.1.1)
+      debug: 4.3.7(supports-color@8.1.1)
       gensync: 1.0.0-beta.2
       json5: 2.2.3
       semver: 6.3.1
@@ -12243,7 +12264,7 @@ snapshots:
       '@babel/traverse': 7.24.7
       '@babel/types': 7.24.7
       convert-source-map: 2.0.0
-      debug: 4.3.5(supports-color@8.1.1)
+      debug: 4.3.7(supports-color@8.1.1)
       gensync: 1.0.0-beta.2
       json5: 2.2.3
       semver: 6.3.1
@@ -12502,7 +12523,7 @@ snapshots:
       '@babel/helper-split-export-declaration': 7.22.6
       '@babel/parser': 7.24.7
       '@babel/types': 7.24.7
-      debug: 4.3.5(supports-color@8.1.1)
+      debug: 4.3.7(supports-color@8.1.1)
       globals: 11.12.0
     transitivePeerDependencies:
       - supports-color
@@ -12517,7 +12538,7 @@ snapshots:
       '@babel/helper-split-export-declaration': 7.24.7
       '@babel/parser': 7.24.7
       '@babel/types': 7.24.7
-      debug: 4.3.5(supports-color@8.1.1)
+      debug: 4.3.7(supports-color@8.1.1)
       globals: 11.12.0
     transitivePeerDependencies:
       - supports-color
@@ -12684,7 +12705,7 @@ snapshots:
 
   '@emnapi/runtime@1.2.0':
     dependencies:
-      tslib: 2.6.3
+      tslib: 2.7.0
     optional: true
 
   '@esbuild/aix-ppc64@0.19.11':
@@ -13093,7 +13114,7 @@ snapshots:
 
   '@fastify/accept-negotiator@2.0.0': {}
 
-  '@fastify/accepts@5.0.0':
+  '@fastify/accepts@5.0.1':
     dependencies:
       accepts: 1.3.8
       fastify-plugin: 5.0.0
@@ -13108,12 +13129,12 @@ snapshots:
 
   '@fastify/busboy@3.0.0': {}
 
-  '@fastify/cookie@10.0.0':
+  '@fastify/cookie@10.0.1':
     dependencies:
       cookie-signature: 1.2.1
       fastify-plugin: 5.0.0
 
-  '@fastify/cors@10.0.0':
+  '@fastify/cors@10.0.1':
     dependencies:
       fastify-plugin: 5.0.0
       mnemonist: 0.39.8
@@ -13122,9 +13143,9 @@ snapshots:
 
   '@fastify/error@4.0.0': {}
 
-  '@fastify/express@4.0.0':
+  '@fastify/express@4.0.1':
     dependencies:
-      express: 4.19.2
+      express: 4.21.0
       fastify-plugin: 5.0.0
     transitivePeerDependencies:
       - supports-color
@@ -13147,7 +13168,7 @@ snapshots:
     dependencies:
       fast-deep-equal: 3.1.3
 
-  '@fastify/multipart@9.0.0':
+  '@fastify/multipart@9.0.1':
     dependencies:
       '@fastify/busboy': 3.0.0
       '@fastify/deepmerge': 2.0.0
@@ -13173,15 +13194,6 @@ snapshots:
       http-errors: 2.0.0
       mime: 3.0.0
 
-  '@fastify/static@8.0.0':
-    dependencies:
-      '@fastify/accept-negotiator': 2.0.0
-      '@fastify/send': 3.1.1
-      content-disposition: 0.5.4
-      fastify-plugin: 5.0.0
-      fastq: 1.17.1
-      glob: 11.0.0
-
   '@fastify/static@8.0.1':
     dependencies:
       '@fastify/accept-negotiator': 2.0.0
@@ -13191,11 +13203,6 @@ snapshots:
       fastq: 1.17.1
       glob: 11.0.0
 
-  '@fastify/view@10.0.0':
-    dependencies:
-      fastify-plugin: 5.0.0
-      toad-cache: 3.7.0
-
   '@fastify/view@10.0.1':
     dependencies:
       fastify-plugin: 5.0.0
@@ -13754,7 +13761,7 @@ snapshots:
       '@napi-rs/canvas-linux-x64-musl': 0.1.56
       '@napi-rs/canvas-win32-x64-msvc': 0.1.56
 
-  '@nestjs/common@10.4.3(reflect-metadata@0.2.2)(rxjs@7.8.1)':
+  '@nestjs/common@10.4.4(reflect-metadata@0.2.2)(rxjs@7.8.1)':
     dependencies:
       iterare: 1.2.1
       reflect-metadata: 0.2.2
@@ -13762,9 +13769,9 @@ snapshots:
       tslib: 2.7.0
       uid: 2.0.2
 
-  '@nestjs/core@10.4.3(@nestjs/common@10.4.3(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.3)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.1)':
+  '@nestjs/core@10.4.4(@nestjs/common@10.4.4(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.4)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.1)':
     dependencies:
-      '@nestjs/common': 10.4.3(reflect-metadata@0.2.2)(rxjs@7.8.1)
+      '@nestjs/common': 10.4.4(reflect-metadata@0.2.2)(rxjs@7.8.1)
       '@nuxtjs/opencollective': 0.3.2(encoding@0.1.13)
       fast-safe-stringify: 2.1.1
       iterare: 1.2.1
@@ -13774,14 +13781,14 @@ snapshots:
       tslib: 2.7.0
       uid: 2.0.2
     optionalDependencies:
-      '@nestjs/platform-express': 10.4.3(@nestjs/common@10.4.3(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.3)
+      '@nestjs/platform-express': 10.4.4(@nestjs/common@10.4.4(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.4)
     transitivePeerDependencies:
       - encoding
 
-  '@nestjs/platform-express@10.4.3(@nestjs/common@10.4.3(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.3)':
+  '@nestjs/platform-express@10.4.4(@nestjs/common@10.4.4(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.4)':
     dependencies:
-      '@nestjs/common': 10.4.3(reflect-metadata@0.2.2)(rxjs@7.8.1)
-      '@nestjs/core': 10.4.3(@nestjs/common@10.4.3(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.3)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.1)
+      '@nestjs/common': 10.4.4(reflect-metadata@0.2.2)(rxjs@7.8.1)
+      '@nestjs/core': 10.4.4(@nestjs/common@10.4.4(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.4)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.1)
       body-parser: 1.20.3
       cors: 2.8.5
       express: 4.21.0
@@ -13790,15 +13797,15 @@ snapshots:
     transitivePeerDependencies:
       - supports-color
 
-  '@nestjs/testing@10.4.3(@nestjs/common@10.4.3(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.3(@nestjs/common@10.4.3(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.3)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.3(@nestjs/common@10.4.3(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.3))':
+  '@nestjs/testing@10.4.4(@nestjs/common@10.4.4(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.4(@nestjs/common@10.4.4(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.4)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.4(@nestjs/common@10.4.4(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.4))':
     dependencies:
-      '@nestjs/common': 10.4.3(reflect-metadata@0.2.2)(rxjs@7.8.1)
-      '@nestjs/core': 10.4.3(@nestjs/common@10.4.3(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.3)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.1)
+      '@nestjs/common': 10.4.4(reflect-metadata@0.2.2)(rxjs@7.8.1)
+      '@nestjs/core': 10.4.4(@nestjs/common@10.4.4(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.4)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.1)
       tslib: 2.7.0
     optionalDependencies:
-      '@nestjs/platform-express': 10.4.3(@nestjs/common@10.4.3(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.3)
+      '@nestjs/platform-express': 10.4.4(@nestjs/common@10.4.4(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.4)
 
-  '@noble/hashes@1.4.0': {}
+  '@noble/hashes@1.5.0': {}
 
   '@nodelib/fs.scandir@2.1.5':
     dependencies:
@@ -14080,27 +14087,27 @@ snapshots:
     dependencies:
       '@peculiar/asn1-schema': 2.3.8
       asn1js: 3.0.5
-      tslib: 2.6.3
+      tslib: 2.7.0
 
   '@peculiar/asn1-ecc@2.3.8':
     dependencies:
       '@peculiar/asn1-schema': 2.3.8
       '@peculiar/asn1-x509': 2.3.8
       asn1js: 3.0.5
-      tslib: 2.6.3
+      tslib: 2.7.0
 
   '@peculiar/asn1-rsa@2.3.8':
     dependencies:
       '@peculiar/asn1-schema': 2.3.8
       '@peculiar/asn1-x509': 2.3.8
       asn1js: 3.0.5
-      tslib: 2.6.3
+      tslib: 2.7.0
 
   '@peculiar/asn1-schema@2.3.8':
     dependencies:
       asn1js: 3.0.5
       pvtsutils: 1.3.5
-      tslib: 2.6.3
+      tslib: 2.7.0
 
   '@peculiar/asn1-x509@2.3.8':
     dependencies:
@@ -14108,7 +14115,7 @@ snapshots:
       asn1js: 3.0.5
       ipaddr.js: 2.2.0
       pvtsutils: 1.3.5
-      tslib: 2.6.3
+      tslib: 2.7.0
 
   '@peertube/http-signature@1.7.0':
     dependencies:
@@ -14338,6 +14345,33 @@ snapshots:
     dependencies:
       '@types/hast': 3.0.4
 
+  '@shikijs/core@1.21.0':
+    dependencies:
+      '@shikijs/engine-javascript': 1.21.0
+      '@shikijs/engine-oniguruma': 1.21.0
+      '@shikijs/types': 1.21.0
+      '@shikijs/vscode-textmate': 9.2.2
+      '@types/hast': 3.0.4
+      hast-util-to-html: 9.0.3
+
+  '@shikijs/engine-javascript@1.21.0':
+    dependencies:
+      '@shikijs/types': 1.21.0
+      '@shikijs/vscode-textmate': 9.2.2
+      oniguruma-to-js: 0.4.3
+
+  '@shikijs/engine-oniguruma@1.21.0':
+    dependencies:
+      '@shikijs/types': 1.21.0
+      '@shikijs/vscode-textmate': 9.2.2
+
+  '@shikijs/types@1.21.0':
+    dependencies:
+      '@shikijs/vscode-textmate': 9.2.2
+      '@types/hast': 3.0.4
+
+  '@shikijs/vscode-textmate@9.2.2': {}
+
   '@sideway/address@4.1.4':
     dependencies:
       '@hapi/hoek': 9.3.0
@@ -14403,21 +14437,21 @@ snapshots:
   '@smithy/abort-controller@2.2.0':
     dependencies:
       '@smithy/types': 2.12.0
-      tslib: 2.6.2
+      tslib: 2.7.0
 
   '@smithy/abort-controller@3.1.1':
     dependencies:
       '@smithy/types': 3.3.0
-      tslib: 2.6.3
+      tslib: 2.7.0
 
   '@smithy/chunked-blob-reader-native@3.0.0':
     dependencies:
       '@smithy/util-base64': 3.0.0
-      tslib: 2.6.3
+      tslib: 2.7.0
 
   '@smithy/chunked-blob-reader@3.0.0':
     dependencies:
-      tslib: 2.6.3
+      tslib: 2.7.0
 
   '@smithy/config-resolver@3.0.5':
     dependencies:
@@ -14425,7 +14459,7 @@ snapshots:
       '@smithy/types': 3.3.0
       '@smithy/util-config-provider': 3.0.0
       '@smithy/util-middleware': 3.0.3
-      tslib: 2.6.3
+      tslib: 2.7.0
 
   '@smithy/core@2.3.1':
     dependencies:
@@ -14436,7 +14470,7 @@ snapshots:
       '@smithy/smithy-client': 3.1.11
       '@smithy/types': 3.3.0
       '@smithy/util-middleware': 3.0.3
-      tslib: 2.6.3
+      tslib: 2.7.0
 
   '@smithy/credential-provider-imds@3.2.0':
     dependencies:
@@ -14444,37 +14478,37 @@ snapshots:
       '@smithy/property-provider': 3.1.3
       '@smithy/types': 3.3.0
       '@smithy/url-parser': 3.0.3
-      tslib: 2.6.3
+      tslib: 2.7.0
 
   '@smithy/eventstream-codec@3.1.2':
     dependencies:
       '@aws-crypto/crc32': 5.2.0
       '@smithy/types': 3.3.0
       '@smithy/util-hex-encoding': 3.0.0
-      tslib: 2.6.3
+      tslib: 2.7.0
 
   '@smithy/eventstream-serde-browser@3.0.5':
     dependencies:
       '@smithy/eventstream-serde-universal': 3.0.4
       '@smithy/types': 3.3.0
-      tslib: 2.6.3
+      tslib: 2.7.0
 
   '@smithy/eventstream-serde-config-resolver@3.0.3':
     dependencies:
       '@smithy/types': 3.3.0
-      tslib: 2.6.3
+      tslib: 2.7.0
 
   '@smithy/eventstream-serde-node@3.0.4':
     dependencies:
       '@smithy/eventstream-serde-universal': 3.0.4
       '@smithy/types': 3.3.0
-      tslib: 2.6.3
+      tslib: 2.7.0
 
   '@smithy/eventstream-serde-universal@3.0.4':
     dependencies:
       '@smithy/eventstream-codec': 3.1.2
       '@smithy/types': 3.3.0
-      tslib: 2.6.3
+      tslib: 2.7.0
 
   '@smithy/fetch-http-handler@3.2.4':
     dependencies:
@@ -14482,52 +14516,52 @@ snapshots:
       '@smithy/querystring-builder': 3.0.3
       '@smithy/types': 3.3.0
       '@smithy/util-base64': 3.0.0
-      tslib: 2.6.3
+      tslib: 2.7.0
 
   '@smithy/hash-blob-browser@3.1.2':
     dependencies:
       '@smithy/chunked-blob-reader': 3.0.0
       '@smithy/chunked-blob-reader-native': 3.0.0
       '@smithy/types': 3.3.0
-      tslib: 2.6.3
+      tslib: 2.7.0
 
   '@smithy/hash-node@3.0.3':
     dependencies:
       '@smithy/types': 3.3.0
       '@smithy/util-buffer-from': 3.0.0
       '@smithy/util-utf8': 3.0.0
-      tslib: 2.6.3
+      tslib: 2.7.0
 
   '@smithy/hash-stream-node@3.1.2':
     dependencies:
       '@smithy/types': 3.3.0
       '@smithy/util-utf8': 3.0.0
-      tslib: 2.6.3
+      tslib: 2.7.0
 
   '@smithy/invalid-dependency@3.0.3':
     dependencies:
       '@smithy/types': 3.3.0
-      tslib: 2.6.3
+      tslib: 2.7.0
 
   '@smithy/is-array-buffer@2.0.0':
     dependencies:
-      tslib: 2.6.3
+      tslib: 2.7.0
 
   '@smithy/is-array-buffer@3.0.0':
     dependencies:
-      tslib: 2.6.3
+      tslib: 2.7.0
 
   '@smithy/md5-js@3.0.3':
     dependencies:
       '@smithy/types': 3.3.0
       '@smithy/util-utf8': 3.0.0
-      tslib: 2.6.3
+      tslib: 2.7.0
 
   '@smithy/middleware-content-length@3.0.5':
     dependencies:
       '@smithy/protocol-http': 4.1.0
       '@smithy/types': 3.3.0
-      tslib: 2.6.3
+      tslib: 2.7.0
 
   '@smithy/middleware-endpoint@3.1.0':
     dependencies:
@@ -14537,7 +14571,7 @@ snapshots:
       '@smithy/types': 3.3.0
       '@smithy/url-parser': 3.0.3
       '@smithy/util-middleware': 3.0.3
-      tslib: 2.6.3
+      tslib: 2.7.0
 
   '@smithy/middleware-retry@3.0.13':
     dependencies:
@@ -14548,25 +14582,25 @@ snapshots:
       '@smithy/types': 3.3.0
       '@smithy/util-middleware': 3.0.3
       '@smithy/util-retry': 3.0.3
-      tslib: 2.6.3
+      tslib: 2.7.0
       uuid: 9.0.1
 
   '@smithy/middleware-serde@3.0.3':
     dependencies:
       '@smithy/types': 3.3.0
-      tslib: 2.6.3
+      tslib: 2.7.0
 
   '@smithy/middleware-stack@3.0.3':
     dependencies:
       '@smithy/types': 3.3.0
-      tslib: 2.6.3
+      tslib: 2.7.0
 
   '@smithy/node-config-provider@3.1.4':
     dependencies:
       '@smithy/property-provider': 3.1.3
       '@smithy/shared-ini-file-loader': 3.1.4
       '@smithy/types': 3.3.0
-      tslib: 2.6.3
+      tslib: 2.7.0
 
   '@smithy/node-http-handler@2.5.0':
     dependencies:
@@ -14582,39 +14616,39 @@ snapshots:
       '@smithy/protocol-http': 4.1.0
       '@smithy/querystring-builder': 3.0.3
       '@smithy/types': 3.3.0
-      tslib: 2.6.3
+      tslib: 2.7.0
 
   '@smithy/property-provider@3.1.3':
     dependencies:
       '@smithy/types': 3.3.0
-      tslib: 2.6.3
+      tslib: 2.7.0
 
   '@smithy/protocol-http@3.3.0':
     dependencies:
       '@smithy/types': 2.12.0
-      tslib: 2.6.2
+      tslib: 2.7.0
 
   '@smithy/protocol-http@4.1.0':
     dependencies:
       '@smithy/types': 3.3.0
-      tslib: 2.6.3
+      tslib: 2.7.0
 
   '@smithy/querystring-builder@2.2.0':
     dependencies:
       '@smithy/types': 2.12.0
       '@smithy/util-uri-escape': 2.2.0
-      tslib: 2.6.2
+      tslib: 2.7.0
 
   '@smithy/querystring-builder@3.0.3':
     dependencies:
       '@smithy/types': 3.3.0
       '@smithy/util-uri-escape': 3.0.0
-      tslib: 2.6.3
+      tslib: 2.7.0
 
   '@smithy/querystring-parser@3.0.3':
     dependencies:
       '@smithy/types': 3.3.0
-      tslib: 2.6.3
+      tslib: 2.7.0
 
   '@smithy/service-error-classification@3.0.3':
     dependencies:
@@ -14623,7 +14657,7 @@ snapshots:
   '@smithy/shared-ini-file-loader@3.1.4':
     dependencies:
       '@smithy/types': 3.3.0
-      tslib: 2.6.3
+      tslib: 2.7.0
 
   '@smithy/signature-v4@4.1.0':
     dependencies:
@@ -14634,7 +14668,7 @@ snapshots:
       '@smithy/util-middleware': 3.0.3
       '@smithy/util-uri-escape': 3.0.0
       '@smithy/util-utf8': 3.0.0
-      tslib: 2.6.3
+      tslib: 2.7.0
 
   '@smithy/smithy-client@3.1.11':
     dependencies:
@@ -14643,49 +14677,49 @@ snapshots:
       '@smithy/protocol-http': 4.1.0
       '@smithy/types': 3.3.0
       '@smithy/util-stream': 3.1.3
-      tslib: 2.6.3
+      tslib: 2.7.0
 
   '@smithy/types@2.12.0':
     dependencies:
-      tslib: 2.6.2
+      tslib: 2.7.0
 
   '@smithy/types@3.3.0':
     dependencies:
-      tslib: 2.6.3
+      tslib: 2.7.0
 
   '@smithy/url-parser@3.0.3':
     dependencies:
       '@smithy/querystring-parser': 3.0.3
       '@smithy/types': 3.3.0
-      tslib: 2.6.3
+      tslib: 2.7.0
 
   '@smithy/util-base64@3.0.0':
     dependencies:
       '@smithy/util-buffer-from': 3.0.0
       '@smithy/util-utf8': 3.0.0
-      tslib: 2.6.3
+      tslib: 2.7.0
 
   '@smithy/util-body-length-browser@3.0.0':
     dependencies:
-      tslib: 2.6.3
+      tslib: 2.7.0
 
   '@smithy/util-body-length-node@3.0.0':
     dependencies:
-      tslib: 2.6.3
+      tslib: 2.7.0
 
   '@smithy/util-buffer-from@2.0.0':
     dependencies:
       '@smithy/is-array-buffer': 2.0.0
-      tslib: 2.6.3
+      tslib: 2.7.0
 
   '@smithy/util-buffer-from@3.0.0':
     dependencies:
       '@smithy/is-array-buffer': 3.0.0
-      tslib: 2.6.3
+      tslib: 2.7.0
 
   '@smithy/util-config-provider@3.0.0':
     dependencies:
-      tslib: 2.6.3
+      tslib: 2.7.0
 
   '@smithy/util-defaults-mode-browser@3.0.13':
     dependencies:
@@ -14693,7 +14727,7 @@ snapshots:
       '@smithy/smithy-client': 3.1.11
       '@smithy/types': 3.3.0
       bowser: 2.11.0
-      tslib: 2.6.3
+      tslib: 2.7.0
 
   '@smithy/util-defaults-mode-node@3.0.13':
     dependencies:
@@ -14703,28 +14737,28 @@ snapshots:
       '@smithy/property-provider': 3.1.3
       '@smithy/smithy-client': 3.1.11
       '@smithy/types': 3.3.0
-      tslib: 2.6.3
+      tslib: 2.7.0
 
   '@smithy/util-endpoints@2.0.5':
     dependencies:
       '@smithy/node-config-provider': 3.1.4
       '@smithy/types': 3.3.0
-      tslib: 2.6.3
+      tslib: 2.7.0
 
   '@smithy/util-hex-encoding@3.0.0':
     dependencies:
-      tslib: 2.6.3
+      tslib: 2.7.0
 
   '@smithy/util-middleware@3.0.3':
     dependencies:
       '@smithy/types': 3.3.0
-      tslib: 2.6.3
+      tslib: 2.7.0
 
   '@smithy/util-retry@3.0.3':
     dependencies:
       '@smithy/service-error-classification': 3.0.3
       '@smithy/types': 3.3.0
-      tslib: 2.6.3
+      tslib: 2.7.0
 
   '@smithy/util-stream@3.1.3':
     dependencies:
@@ -14735,31 +14769,31 @@ snapshots:
       '@smithy/util-buffer-from': 3.0.0
       '@smithy/util-hex-encoding': 3.0.0
       '@smithy/util-utf8': 3.0.0
-      tslib: 2.6.3
+      tslib: 2.7.0
 
   '@smithy/util-uri-escape@2.2.0':
     dependencies:
-      tslib: 2.6.2
+      tslib: 2.7.0
 
   '@smithy/util-uri-escape@3.0.0':
     dependencies:
-      tslib: 2.6.3
+      tslib: 2.7.0
 
   '@smithy/util-utf8@2.0.0':
     dependencies:
       '@smithy/util-buffer-from': 2.0.0
-      tslib: 2.6.3
+      tslib: 2.7.0
 
   '@smithy/util-utf8@3.0.0':
     dependencies:
       '@smithy/util-buffer-from': 3.0.0
-      tslib: 2.6.3
+      tslib: 2.7.0
 
   '@smithy/util-waiter@3.1.2':
     dependencies:
       '@smithy/abort-controller': 3.1.1
       '@smithy/types': 3.3.0
-      tslib: 2.6.3
+      tslib: 2.7.0
 
   '@sqltools/formatter@1.2.5': {}
 
@@ -16436,7 +16470,7 @@ snapshots:
 
   agent-base@6.0.2:
     dependencies:
-      debug: 4.3.5(supports-color@8.1.1)
+      debug: 4.3.7(supports-color@8.1.1)
     transitivePeerDependencies:
       - supports-color
     optional: true
@@ -16671,7 +16705,7 @@ snapshots:
     dependencies:
       pvtsutils: 1.3.5
       pvutils: 1.1.3
-      tslib: 2.6.3
+      tslib: 2.7.0
 
   assert-never@1.2.1: {}
 
@@ -16838,23 +16872,6 @@ snapshots:
 
   bn.js@4.12.0: {}
 
-  body-parser@1.20.2:
-    dependencies:
-      bytes: 3.1.2
-      content-type: 1.0.5
-      debug: 2.6.9
-      depd: 2.0.0
-      destroy: 1.2.0
-      http-errors: 2.0.0
-      iconv-lite: 0.4.24
-      on-finished: 2.4.1
-      qs: 6.11.0
-      raw-body: 2.5.2
-      type-is: 1.6.18
-      unpipe: 1.0.0
-    transitivePeerDependencies:
-      - supports-color
-
   body-parser@1.20.3:
     dependencies:
       bytes: 3.1.2
@@ -17118,6 +17135,10 @@ snapshots:
 
   char-regex@1.0.2: {}
 
+  character-entities-html4@2.1.0: {}
+
+  character-entities-legacy@3.0.0: {}
+
   character-entities@2.0.2: {}
 
   character-parser@2.2.0:
@@ -17307,6 +17328,8 @@ snapshots:
     dependencies:
       delayed-stream: 1.0.0
 
+  comma-separated-tokens@2.0.3: {}
+
   commander@10.0.1: {}
 
   commander@12.1.0: {}
@@ -18626,42 +18649,6 @@ snapshots:
 
   exponential-backoff@3.1.1: {}
 
-  express@4.19.2:
-    dependencies:
-      accepts: 1.3.8
-      array-flatten: 1.1.1
-      body-parser: 1.20.2
-      content-disposition: 0.5.4
-      content-type: 1.0.5
-      cookie: 0.6.0
-      cookie-signature: 1.0.6
-      debug: 2.6.9
-      depd: 2.0.0
-      encodeurl: 1.0.2
-      escape-html: 1.0.3
-      etag: 1.8.1
-      finalhandler: 1.2.0
-      fresh: 0.5.2
-      http-errors: 2.0.0
-      merge-descriptors: 1.0.1
-      methods: 1.1.2
-      on-finished: 2.4.1
-      parseurl: 1.3.3
-      path-to-regexp: 0.1.7
-      proxy-addr: 2.0.7
-      qs: 6.11.0
-      range-parser: 1.2.1
-      safe-buffer: 5.2.1
-      send: 0.18.0
-      serve-static: 1.15.0
-      setprototypeof: 1.2.0
-      statuses: 2.0.1
-      type-is: 1.6.18
-      utils-merge: 1.0.1
-      vary: 1.1.2
-    transitivePeerDependencies:
-      - supports-color
-
   express@4.21.0:
     dependencies:
       accepts: 1.3.8
@@ -18865,18 +18852,6 @@ snapshots:
     dependencies:
       to-regex-range: 5.0.1
 
-  finalhandler@1.2.0:
-    dependencies:
-      debug: 2.6.9
-      encodeurl: 1.0.2
-      escape-html: 1.0.3
-      on-finished: 2.4.1
-      parseurl: 1.3.3
-      statuses: 2.0.1
-      unpipe: 1.0.0
-    transitivePeerDependencies:
-      - supports-color
-
   finalhandler@1.3.1:
     dependencies:
       debug: 2.6.9
@@ -19346,10 +19321,28 @@ snapshots:
     dependencies:
       '@types/hast': 3.0.4
 
+  hast-util-to-html@9.0.3:
+    dependencies:
+      '@types/hast': 3.0.4
+      '@types/unist': 3.0.2
+      ccount: 2.0.1
+      comma-separated-tokens: 2.0.3
+      hast-util-whitespace: 3.0.0
+      html-void-elements: 3.0.0
+      mdast-util-to-hast: 13.2.0
+      property-information: 6.5.0
+      space-separated-tokens: 2.0.2
+      stringify-entities: 4.0.4
+      zwitch: 2.0.4
+
   hast-util-to-string@3.0.0:
     dependencies:
       '@types/hast': 3.0.4
 
+  hast-util-whitespace@3.0.0:
+    dependencies:
+      '@types/hast': 3.0.4
+
   he@1.2.0: {}
 
   headers-polyfill@4.0.2: {}
@@ -19376,6 +19369,8 @@ snapshots:
 
   html-tags@3.2.0: {}
 
+  html-void-elements@3.0.0: {}
+
   htmlescape@1.1.1: {}
 
   htmlparser2@5.0.1:
@@ -19832,7 +19827,7 @@ snapshots:
 
   istanbul-lib-source-maps@4.0.1:
     dependencies:
-      debug: 4.3.5(supports-color@8.1.1)
+      debug: 4.3.7(supports-color@8.1.1)
       istanbul-lib-coverage: 3.2.2
       source-map: 0.6.1
     transitivePeerDependencies:
@@ -20722,6 +20717,18 @@ snapshots:
       '@types/mdast': 4.0.3
       unist-util-is: 6.0.0
 
+  mdast-util-to-hast@13.2.0:
+    dependencies:
+      '@types/hast': 3.0.4
+      '@types/mdast': 4.0.3
+      '@ungap/structured-clone': 1.2.0
+      devlop: 1.1.0
+      micromark-util-sanitize-uri: 2.0.0
+      trim-lines: 3.0.1
+      unist-util-position: 5.0.0
+      unist-util-visit: 5.0.0
+      vfile: 6.0.1
+
   mdast-util-to-markdown@2.1.0:
     dependencies:
       '@types/mdast': 4.0.3
@@ -20770,8 +20777,6 @@ snapshots:
       type-fest: 0.18.1
       yargs-parser: 20.2.9
 
-  merge-descriptors@1.0.1: {}
-
   merge-descriptors@1.0.3: {}
 
   merge-stream@2.0.0: {}
@@ -21522,6 +21527,10 @@ snapshots:
     dependencies:
       mimic-fn: 4.0.0
 
+  oniguruma-to-js@0.4.3:
+    dependencies:
+      regex: 4.3.3
+
   open@8.4.2:
     dependencies:
       define-lazy-prop: 2.0.0
@@ -21565,9 +21574,9 @@ snapshots:
 
   ospath@1.2.2: {}
 
-  otpauth@9.3.2:
+  otpauth@9.3.4:
     dependencies:
-      '@noble/hashes': 1.4.0
+      '@noble/hashes': 1.5.0
 
   outvariant@1.4.2: {}
 
@@ -21686,8 +21695,6 @@ snapshots:
 
   path-to-regexp@0.1.10: {}
 
-  path-to-regexp@0.1.7: {}
-
   path-to-regexp@1.8.0:
     dependencies:
       isarray: 0.0.1
@@ -22108,6 +22115,8 @@ snapshots:
       object-assign: 4.1.1
       react-is: 16.13.1
 
+  property-information@6.5.0: {}
+
   proto-list@1.2.4: {}
 
   proxy-addr@2.0.7:
@@ -22211,7 +22220,7 @@ snapshots:
 
   pvtsutils@1.3.5:
     dependencies:
-      tslib: 2.6.3
+      tslib: 2.7.0
 
   pvutils@1.1.3: {}
 
@@ -22221,10 +22230,6 @@ snapshots:
       pngjs: 5.0.0
       yargs: 15.4.1
 
-  qs@6.11.0:
-    dependencies:
-      side-channel: 1.0.6
-
   qs@6.13.0:
     dependencies:
       side-channel: 1.0.6
@@ -22419,6 +22424,8 @@ snapshots:
 
   regenerator-runtime@0.14.0: {}
 
+  regex@4.3.3: {}
+
   regexp.prototype.flags@1.5.0:
     dependencies:
       call-bind: 1.0.2
@@ -22705,24 +22712,6 @@ snapshots:
 
   semver@7.6.3: {}
 
-  send@0.18.0:
-    dependencies:
-      debug: 2.6.9
-      depd: 2.0.0
-      destroy: 1.2.0
-      encodeurl: 1.0.2
-      escape-html: 1.0.3
-      etag: 1.8.1
-      fresh: 0.5.2
-      http-errors: 2.0.0
-      mime: 1.6.0
-      ms: 2.1.3
-      on-finished: 2.4.1
-      range-parser: 1.2.1
-      statuses: 2.0.1
-    transitivePeerDependencies:
-      - supports-color
-
   send@0.19.0:
     dependencies:
       debug: 2.6.9
@@ -22741,15 +22730,6 @@ snapshots:
     transitivePeerDependencies:
       - supports-color
 
-  serve-static@1.15.0:
-    dependencies:
-      encodeurl: 1.0.2
-      escape-html: 1.0.3
-      parseurl: 1.3.3
-      send: 0.18.0
-    transitivePeerDependencies:
-      - supports-color
-
   serve-static@1.16.2:
     dependencies:
       encodeurl: 2.0.0
@@ -22831,6 +22811,15 @@ snapshots:
       '@shikijs/core': 1.12.0
       '@types/hast': 3.0.4
 
+  shiki@1.21.0:
+    dependencies:
+      '@shikijs/core': 1.21.0
+      '@shikijs/engine-javascript': 1.21.0
+      '@shikijs/engine-oniguruma': 1.21.0
+      '@shikijs/types': 1.21.0
+      '@shikijs/vscode-textmate': 9.2.2
+      '@types/hast': 3.0.4
+
   shimmer@1.2.1: {}
 
   side-channel@1.0.4:
@@ -22956,7 +22945,7 @@ snapshots:
   socks-proxy-agent@8.0.2:
     dependencies:
       agent-base: 7.1.0
-      debug: 4.3.5(supports-color@8.1.1)
+      debug: 4.3.7(supports-color@8.1.1)
       socks: 2.7.1
     transitivePeerDependencies:
       - supports-color
@@ -23194,6 +23183,11 @@ snapshots:
     dependencies:
       safe-buffer: 5.2.1
 
+  stringify-entities@4.0.4:
+    dependencies:
+      character-entities-html4: 2.1.0
+      character-entities-legacy: 3.0.0
+
   stringz@2.1.0:
     dependencies:
       char-regex: 1.0.2
@@ -23426,6 +23420,8 @@ snapshots:
 
   trace-redirect@1.0.6: {}
 
+  trim-lines@3.0.1: {}
+
   trim-newlines@3.0.1: {}
 
   trim-repeated@2.0.0:
@@ -23677,6 +23673,10 @@ snapshots:
     dependencies:
       '@types/unist': 3.0.2
 
+  unist-util-position@5.0.0:
+    dependencies:
+      '@types/unist': 3.0.2
+
   unist-util-stringify-position@4.0.0:
     dependencies:
       '@types/unist': 3.0.2

From 83db116c46e64ad6a9a479cbd00e96030821c1e9 Mon Sep 17 00:00:00 2001
From: syuilo <4439005+syuilo@users.noreply.github.com>
Date: Thu, 3 Oct 2024 15:06:04 +0900
Subject: [PATCH 010/121] enhance(backend): notify new login (#14673)

* wip

* Update CHANGELOG.md

* wip

* fix

* Update index.d.ts

* Update SigninService.ts

* Update MkNotification.vue
---
 CHANGELOG.md                                  |   2 +-
 locales/index.d.ts                            |   8 +++++++
 locales/ja-JP.yml                             |   2 ++
 .../backend/assets/tabler-badges/login-2.png  | Bin 0 -> 3770 bytes
 packages/backend/src/models/Notification.ts   |   6 +++++-
 .../src/models/json-schema/notification.ts    |  10 +++++++++
 .../backend/src/server/api/SigninService.ts   |  20 +++++++++++++++---
 packages/backend/src/types.ts                 |   2 ++
 packages/frontend-shared/js/const.ts          |   1 +
 .../src/components/MkNotification.vue         |  13 ++++++++++--
 packages/misskey-js/src/autogen/types.ts      |  17 ++++++++++-----
 .../sw/src/scripts/create-notification.ts     |   6 ++++++
 packages/sw/src/types.ts                      |   3 ++-
 13 files changed, 77 insertions(+), 13 deletions(-)
 create mode 100644 packages/backend/assets/tabler-badges/login-2.png

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8f0fd24c44..72c3b22d69 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,7 +7,7 @@
 - Enhance: フォロワーへのメッセージ欄のデザイン改良
 
 ### Server
--
+- Enhance: セキュリティ向上のため、ログイン時にメール通知を行うように
 
 
 ## 2024.9.0
diff --git a/locales/index.d.ts b/locales/index.d.ts
index 29c93453ff..0a9123f03d 100644
--- a/locales/index.d.ts
+++ b/locales/index.d.ts
@@ -9285,6 +9285,10 @@ export interface Locale extends ILocale {
          * {x}のエクスポートが完了しました
          */
         "exportOfXCompleted": ParameterizedString<"x">;
+        /**
+         * ログインがありました
+         */
+        "login": string;
         "_types": {
             /**
              * すべて
@@ -9342,6 +9346,10 @@ export interface Locale extends ILocale {
              * エクスポートが完了した
              */
             "exportCompleted": string;
+            /**
+             * ログイン
+             */
+            "login": string;
             /**
              * 通知のテスト
              */
diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml
index 678af6987c..cfbe0dcc75 100644
--- a/locales/ja-JP.yml
+++ b/locales/ja-JP.yml
@@ -2451,6 +2451,7 @@ _notification:
   followedBySomeUsers: "{n}人にフォローされました"
   flushNotification: "通知の履歴をリセットする"
   exportOfXCompleted: "{x}のエクスポートが完了しました"
+  login: "ログインがありました"
 
   _types:
     all: "すべて"
@@ -2467,6 +2468,7 @@ _notification:
     roleAssigned: "ロールが付与された"
     achievementEarned: "実績の獲得"
     exportCompleted: "エクスポートが完了した"
+    login: "ログイン"
     test: "通知のテスト"
     app: "連携アプリからの通知"
 
diff --git a/packages/backend/assets/tabler-badges/login-2.png b/packages/backend/assets/tabler-badges/login-2.png
new file mode 100644
index 0000000000000000000000000000000000000000..f3ca8de3ddd0125ef523a249dd0ebd8d086c7566
GIT binary patch
literal 3770
zcmd^C`8yQ+7XQvL(%8$MEKw-CEXh(MTklwFkjWS{wy{MrgoYPMl<bvAl*$@4vJCS|
zh(eaJOc>&|44E?4EO&Z;yMMvG_qjiOzvnsM=RD_p&Uv2i`J8jx&c=cdE(r$!@L5@&
zb6`v6@59Z-zKv1y>1+WBb+AByif*ZS0Qj$2ojZGxfLVGxR<2MhWd3rWi9Q@H0H+9*
z34e4U9th^7deJ#~l@H#~4hCHQxWau9s_Nn!o%(w1_w%njc9<ut-o=ewP>ET6EH^Ut
ze%C+v34mv!=rz?3GW6k3C;*EjLK8#)z2$>^m@vQ^{x53uuhJVKDL=laPsy~45AB6`
zOXrN;RJZP$VC?Nqnl&$f>-*^|L5#j;bO;5aP1*#5XJl@bL!m5_c5xZ?n*x*o^d)gV
zqF8ctiE`{}Ll^fW`68ogL1J?g<#Kg9kV5N9T!mdWrwtoq!X${EDWS-$=2YK&#~>wH
z{Iq}*ewKxRFt-kT^!asf^>g1;F;q)ZM%Fe);rmH|Dcx~%d+0gfTDmzxEw<5;EHMTI
z*N7_vxbg;@pgY-pro0qUT$9Y7Jg8mC8zk8a3zgLscOb(xtO5`1pIi9FvA8qab>Z^f
zQyff*!M{y78dqF!CKuHFKN@|zxDWiw`jf{oX4KrwruG;YR^~)-X@|uSuRTuKW(BDO
zXlR6*%TBwoicp49+QLg!KnjO3FgL7g%#bzGTv#+-BaAet9eT3vtwOUu+z1Ri#+bXI
zjkGNe5@1uk9K@w*Z`RxX>hr}+dvW4YS<4*01eawwDL?>Kk5RkcV#$^X!rX@wAj56S
zH=DLKCXS8L?58_RqQ1${{vwc2r`A@R+^Y+62ijJ+$?Cim-Dg>@I*lpm3r$6-m;Wf@
z?yYXT-z;r0_dx}alwS_9PK_1a&<#<&H&w@Re$GIgxQhAt(3qpLNw-p-U{oMY;^)GW
ztqhSuTjdow3IUvz6JEEXN1#>XcxlE&RnRrE)Vw^wuvB`@=O|$8c<}}2O0XO<Z)2l-
zj2982fMMhvkXSXJi%HfJD$6kfm!GqmtuMwH_(a?}!fNuEG%(DlU-hhXA5syIEl~bN
zn{}B$q?2<xn`<o8OYxkHFb&X<-KF<VhZ6O2i^@nEe;Sx;7L+Y&-rHrThPYD?sTvPg
zO?i@2csX7)i~~zvb`d|TZd%8!y1+$=oOr7dhe-?(av9(wt~TM_Imx^;N8Y?R1Q^jn
zE!>e3on}7vdVu<?B0-3ym3gQ4X&@+Dcjb|v!zqasnvDVH<%a*ZKq#HK#ZQDrvaN2!
zY2CgIRfieAq-v9}*&@HO7}P;PMGu6aM?@M9!QWL&>=cy;9~Fz;_!ILqQwZqeE>RV6
zb*~%N(GuPMKI36U5dhP3zEf0HZ~yw&fgRq)Tl8@^6`=$eoqO;g$#=EwbkBUvha99{
z5=37PX|H^#QGR$OHQtfIvJvD67FM;Q4I=H0h8~EzsSTxv$BGj(;)JuZk<TWYPpWAc
zhZ0&&L^wC^Ui7$56ekv3oj0HDOj0cR@hL#E&gba5tC2wI{0c*=l>HO)D^5tG=|<9-
zY0X0d=cNQJ%j%o`0tVFjeete{{FmImZ}js9`)YOla_$ei<OpUXB+Jdu{D>ky?;W)8
z0BV7Yu6?yp4vYC-gVa{g$OHNC%lC(1dl_ezEEj#`RS4z<)X_Hi^Yy3jR$z#>@8)sB
z&L~};G1dgPXz85!_Gea#hqafaX1!!?LpG48O=K?AyFj>KAy2{sP%mcpBr1W{<_>;}
zoM^FqIrPl7)mcC_xZQJ08MFc^FJnoPJvKZ^A~qoET;>_r^+a(ZN2*2tb@(D&6VM`(
zEdPQpa%+NpkM8~*ATrG70Z6L?Uk)f$9c%@8l;C1)B}Gk;qlz4!lzT4rFdMLfmpq>g
zte#yK+b#1LKKWaVqZ>Z$ZHlI$k@7Id9RMXNT5JE(ZTuLVM~a2+NqTO7tSbP9a?FK^
zCpN{c%4Wh$HUt@Q2;ojzy1w-Y3o|Y;a}_WTqnv^!r|hvFf7{X3JyV)b6+<cE=UiWl
zy*9Pp|G+M`piR6vw1bBjl;pRgYj|r^s5%!aU8DgfG-BIn%;e3~YpEhk!saP3p>&|%
z+jIRu;I@9-2KhBVh%&XqyQeVB-^IZRWq+1OOa9E)yk}mXpggDNaNUkFT1qv3+Z}Ws
zanJk#!q%$C%2tWO9c8K_Ky)JWE#KY>^}7n&HEP>vc_)xU(c_$v<M%8P3fzuP`H}jM
z^WMRr_u3UFCexMan$g*6Q08OAiiU+29A5J6hu}V7D;_7KjZA7)4i}-f^9t}zT@_;Z
zXn9!dBg_>M+xwk{7Bmv_S;@!}+(|F7yH<bc2N3o|bCp>c#|{!^7FZ2kZrB(HN~+Cz
zo~|}D=!!zULEFNOtx`IUvu$iiWR6!I)YdMKA712F2g^&#RPqD_0|EmkqfgiJG=j#;
zcG_B$A_4NQJav6>IFJct<{Jl%Fb+?`=-H0rl@}UXcqqRj$gcOo6UB(Mk%&tErQeQb
z)*Z>Paod^cA<J@qvV}KsJ;#89DV-nG-!*thrEJ^1<-rjMn|U^nNzhU~$xr<17v=;H
z_03&^Ies5>Wg{?VC>YmXj?Fs6>liCW%%P=m^&GES2)Lg`htX>Sr$W0#oHe5>xYU7q
ztsGxIin8U$U>F`}gYh1F^JR9Llqyxtm1Vts<<Z4ctxOrW-P&a_-^u;KC<26VfAhGm
ze69bLu}z;@Ylw^NF8!-jrSdf#0Wv&t4fVbt!g&hqon(~X^opH{)6q|w%i2uoc>=<J
zN041~$gog+a-pj1=BTf$|0bzJjR_4l>y!#5IeSMo@pMe_JU`e77P(E|_Ga4}M2FR#
zUn-3CN}k}ufVE)fFS+c;h`5lofcyE3m5y}u1ZE|wf7bdjLNc}Np(#zYe^y?#$Cve`
zZIO25sMw{PuAKAj9K_R(tLF+-QmdfY`6+bJ9Yg7p_iyx@=)2mTmaC*$s4$LMCGsZ9
zrPgOD91G@=Z?zN~_4|i7O#h#V<KL*TMz>|wDM)L;+aewfcOFuWf0kzcD55h2ed&H%
zXz03EU%~ggGhW;2t*^$;-RyS<#!In#H+#31Zo29@44nuNJG+vdTwBfjnzk2M32_dB
z<B9J-J;`~B<m{^TwJgD@2aIsu+=NgfKbC*)-AUz+{17Ijf?=&0Z~*F{0;`|e|GHNP
z^&%ZFS(~qEMRo6zHph~E4V%80mf#|-`plUd)mhz<WZGt78rZgbdP}k6M&k7OH>+|N
zd>=fFuHtX>HB={<b!*&hkqq=Ya?f`}?mkhaHv4YQJ)Mkh^68(j6RW>RoZ&4!)}!~P
zA1S%ibrH94GkR8Ozl^$Uc=Sw(*?!o?&Gqj;o&3H<#9J3YhAYZ2{XPx6x2K<-nac=8
z7A|!zoJu8irCVmh<0oqNVH-jsUD<k?B^4fTvIeJ+j^c!WPjPRUUvhM+$VSdQH2gyy
zkBvDViZQh+=1rgCA5Z;+xyfPI9_wH5v6h(FRVk_>KOZ@gh>76B-aJj15L;V@sD5>j
z{V=d;c~@w-=SBX#@0&R5=^OC)9iK@@2e62DmR%bR$FYQ+9=nB$6QfmS8SZs2j;<%2
zS~#9|08@de)XZFe&%#H(4m#n>?l@u3g_GFitB6;+YXC21H=LnEw{?C*e(oXVevcbO
zA9m-9A~nvMw5vW}87<$s@(OLXZoo$=Rr$fVp6k#)xBFtyb6=*`BSpY0O5fVuyZnqD
zT{m*X)fNy)txdC#>dL0tr#TZXV!wy+QWWIUcZsg+8sq4{rWZEr{ii#txACtp$%0Dl
z?i$s@8hev|rX<g1#Y(=>{Lh--HruyQyV83Kwj;J>V;B#R_a$yR9-09M%woH|M5qLN
z7fM0NDGKLa1VqJzsguGTF97f^gP$s-6hncbvT#Y^8FLuW_e6E<haF>ETNUc_!L*J8
zibN^GMoHr(0JiWCzDjBK5YX)`5`u`ef&qeql8tF>3LFHty-GU>N3yLm5LYGPSJ*+c
w9TW2GunrKAFPz+OCtCysd9RQECn3?jgP*AhjJT}Dv6oR`WoC1(0)dVD7ZGyJrvLx|

literal 0
HcmV?d00001

diff --git a/packages/backend/src/models/Notification.ts b/packages/backend/src/models/Notification.ts
index c1d3d42134..b7f8e94d69 100644
--- a/packages/backend/src/models/Notification.ts
+++ b/packages/backend/src/models/Notification.ts
@@ -3,12 +3,12 @@
  * SPDX-License-Identifier: AGPL-3.0-only
  */
 
+import { userExportableEntities } from '@/types.js';
 import { MiUser } from './User.js';
 import { MiNote } from './Note.js';
 import { MiAccessToken } from './AccessToken.js';
 import { MiRole } from './Role.js';
 import { MiDriveFile } from './DriveFile.js';
-import { userExportableEntities } from '@/types.js';
 
 export type MiNotification = {
 	type: 'note';
@@ -86,6 +86,10 @@ export type MiNotification = {
 	createdAt: string;
 	exportedEntity: typeof userExportableEntities[number];
 	fileId: MiDriveFile['id'];
+} | {
+	type: 'login';
+	id: string;
+	createdAt: string;
 } | {
 	type: 'app';
 	id: string;
diff --git a/packages/backend/src/models/json-schema/notification.ts b/packages/backend/src/models/json-schema/notification.ts
index 2645010491..cddaf4bc83 100644
--- a/packages/backend/src/models/json-schema/notification.ts
+++ b/packages/backend/src/models/json-schema/notification.ts
@@ -322,6 +322,16 @@ export const packedNotificationSchema = {
 				format: 'id',
 			},
 		},
+	}, {
+		type: 'object',
+		properties: {
+			...baseSchema.properties,
+			type: {
+				type: 'string',
+				optional: false, nullable: false,
+				enum: ['login'],
+			},
+		},
 	}, {
 		type: 'object',
 		properties: {
diff --git a/packages/backend/src/server/api/SigninService.ts b/packages/backend/src/server/api/SigninService.ts
index 70306c3113..4b041f373f 100644
--- a/packages/backend/src/server/api/SigninService.ts
+++ b/packages/backend/src/server/api/SigninService.ts
@@ -5,12 +5,14 @@
 
 import { Inject, Injectable } from '@nestjs/common';
 import { DI } from '@/di-symbols.js';
-import type { SigninsRepository } from '@/models/_.js';
+import type { SigninsRepository, UserProfilesRepository } from '@/models/_.js';
 import { IdService } from '@/core/IdService.js';
 import type { MiLocalUser } from '@/models/User.js';
 import { GlobalEventService } from '@/core/GlobalEventService.js';
 import { SigninEntityService } from '@/core/entities/SigninEntityService.js';
 import { bindThis } from '@/decorators.js';
+import { EmailService } from '@/core/EmailService.js';
+import { NotificationService } from '@/core/NotificationService.js';
 import type { FastifyRequest, FastifyReply } from 'fastify';
 
 @Injectable()
@@ -19,7 +21,12 @@ export class SigninService {
 		@Inject(DI.signinsRepository)
 		private signinsRepository: SigninsRepository,
 
+		@Inject(DI.userProfilesRepository)
+		private userProfilesRepository: UserProfilesRepository,
+
 		private signinEntityService: SigninEntityService,
+		private emailService: EmailService,
+		private notificationService: NotificationService,
 		private idService: IdService,
 		private globalEventService: GlobalEventService,
 	) {
@@ -28,7 +35,8 @@ export class SigninService {
 	@bindThis
 	public signin(request: FastifyRequest, reply: FastifyReply, user: MiLocalUser) {
 		setImmediate(async () => {
-			// Append signin history
+			this.notificationService.createNotification(user.id, 'login', {});
+
 			const record = await this.signinsRepository.insertOne({
 				id: this.idService.gen(),
 				userId: user.id,
@@ -37,8 +45,14 @@ export class SigninService {
 				success: true,
 			});
 
-			// Publish signin event
 			this.globalEventService.publishMainStream(user.id, 'signin', await this.signinEntityService.pack(record));
+
+			const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id });
+			if (profile.email && profile.emailVerified) {
+				this.emailService.sendEmail(profile.email, 'New login / ログインがありました',
+					'There is a new login. If you do not recognize this login, update the security status of your account, including changing your password. / 新しいログインがありました。このログインに心当たりがない場合は、パスワードを変更するなど、アカウントのセキュリティ状態を更新してください。',
+					'There is a new login. If you do not recognize this login, update the security status of your account, including changing your password. / 新しいログインがありました。このログインに心当たりがない場合は、パスワードを変更するなど、アカウントのセキュリティ状態を更新してください。');
+			}
 		});
 
 		reply.code(200);
diff --git a/packages/backend/src/types.ts b/packages/backend/src/types.ts
index 5854c6b392..0389143daf 100644
--- a/packages/backend/src/types.ts
+++ b/packages/backend/src/types.ts
@@ -17,6 +17,7 @@
  * roleAssigned - ロールが付与された
  * achievementEarned - 実績を獲得
  * exportCompleted - エクスポートが完了
+ * login - ログイン
  * app - アプリ通知
  * test - テスト通知(サーバー側)
  */
@@ -34,6 +35,7 @@ export const notificationTypes = [
 	'roleAssigned',
 	'achievementEarned',
 	'exportCompleted',
+	'login',
 	'app',
 	'test',
 ] as const;
diff --git a/packages/frontend-shared/js/const.ts b/packages/frontend-shared/js/const.ts
index aec4a4a58b..4fe5cbb205 100644
--- a/packages/frontend-shared/js/const.ts
+++ b/packages/frontend-shared/js/const.ts
@@ -68,6 +68,7 @@ export const notificationTypes = [
 	'roleAssigned',
 	'achievementEarned',
 	'exportCompleted',
+	'login',
 	'test',
 	'app',
 ] as const;
diff --git a/packages/frontend/src/components/MkNotification.vue b/packages/frontend/src/components/MkNotification.vue
index 12c2974de4..b27d883b85 100644
--- a/packages/frontend/src/components/MkNotification.vue
+++ b/packages/frontend/src/components/MkNotification.vue
@@ -7,13 +7,12 @@ SPDX-License-Identifier: AGPL-3.0-only
 <div :class="$style.root">
 	<div :class="$style.head">
 		<MkAvatar v-if="['pollEnded', 'note'].includes(notification.type) && 'note' in notification" :class="$style.icon" :user="notification.note.user" link preview/>
-		<MkAvatar v-else-if="['roleAssigned', 'achievementEarned'].includes(notification.type)" :class="$style.icon" :user="$i" link preview/>
+		<MkAvatar v-else-if="['roleAssigned', 'achievementEarned', 'exportCompleted', 'login'].includes(notification.type)" :class="$style.icon" :user="$i" link preview/>
 		<div v-else-if="notification.type === 'reaction:grouped' && notification.note.reactionAcceptance === 'likeOnly'" :class="[$style.icon, $style.icon_reactionGroupHeart]"><i class="ti ti-heart" style="line-height: 1;"></i></div>
 		<div v-else-if="notification.type === 'reaction:grouped'" :class="[$style.icon, $style.icon_reactionGroup]"><i class="ti ti-plus" style="line-height: 1;"></i></div>
 		<div v-else-if="notification.type === 'renote:grouped'" :class="[$style.icon, $style.icon_renoteGroup]"><i class="ti ti-repeat" style="line-height: 1;"></i></div>
 		<img v-else-if="notification.type === 'test'" :class="$style.icon" :src="infoImageUrl"/>
 		<MkAvatar v-else-if="'user' in notification" :class="$style.icon" :user="notification.user" link preview/>
-		<MkAvatar v-else-if="notification.type === 'exportCompleted'" :class="$style.icon" :user="$i" link preview/>
 		<img v-else-if="'icon' in notification && notification.icon != null" :class="[$style.icon, $style.icon_app]" :src="notification.icon" alt=""/>
 		<div
 			:class="[$style.subIcon, {
@@ -27,6 +26,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 				[$style.t_pollEnded]: notification.type === 'pollEnded',
 				[$style.t_achievementEarned]: notification.type === 'achievementEarned',
 				[$style.t_exportCompleted]: notification.type === 'exportCompleted',
+				[$style.t_login]: notification.type === 'login',
 				[$style.t_roleAssigned]: notification.type === 'roleAssigned' && notification.role.iconUrl == null,
 			}]"
 		>
@@ -40,6 +40,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 			<i v-else-if="notification.type === 'pollEnded'" class="ti ti-chart-arrows"></i>
 			<i v-else-if="notification.type === 'achievementEarned'" class="ti ti-medal"></i>
 			<i v-else-if="notification.type === 'exportCompleted'" class="ti ti-archive"></i>
+			<i v-else-if="notification.type === 'login'" class="ti ti-login-2"></i>
 			<template v-else-if="notification.type === 'roleAssigned'">
 				<img v-if="notification.role.iconUrl" style="height: 1.3em; vertical-align: -22%;" :src="notification.role.iconUrl" alt=""/>
 				<i v-else class="ti ti-badges"></i>
@@ -59,6 +60,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 			<span v-else-if="notification.type === 'note'">{{ i18n.ts._notification.newNote }}: <MkUserName :user="notification.note.user"/></span>
 			<span v-else-if="notification.type === 'roleAssigned'">{{ i18n.ts._notification.roleAssigned }}</span>
 			<span v-else-if="notification.type === 'achievementEarned'">{{ i18n.ts._notification.achievementEarned }}</span>
+			<span v-else-if="notification.type === 'login'">{{ i18n.ts._notification.login }}</span>
 			<span v-else-if="notification.type === 'test'">{{ i18n.ts._notification.testNotification }}</span>
 			<span v-else-if="notification.type === 'exportCompleted'">{{ i18n.tsx._notification.exportOfXCompleted({ x: exportEntityName[notification.exportedEntity] }) }}</span>
 			<MkA v-else-if="notification.type === 'follow' || notification.type === 'mention' || notification.type === 'reply' || notification.type === 'renote' || notification.type === 'quote' || notification.type === 'reaction' || notification.type === 'receiveFollowRequest' || notification.type === 'followRequestAccepted'" v-user-preview="notification.user.id" :class="$style.headerName" :to="userPage(notification.user)"><MkUserName :user="notification.user"/></MkA>
@@ -225,6 +227,7 @@ function getActualReactedUsersCount(notification: Misskey.entities.Notification)
 	--eventReactionHeart: var(--love);
 	--eventReaction: #e99a0b;
 	--eventAchievement: #cb9a11;
+	--eventLogin: #007aff;
 	--eventOther: #88a6b7;
 }
 
@@ -346,6 +349,12 @@ function getActualReactedUsersCount(notification: Misskey.entities.Notification)
 	pointer-events: none;
 }
 
+.t_login {
+	padding: 3px;
+	background: var(--eventLogin);
+	pointer-events: none;
+}
+
 .tail {
 	flex: 1;
 	min-width: 0;
diff --git a/packages/misskey-js/src/autogen/types.ts b/packages/misskey-js/src/autogen/types.ts
index 6aaeabec7b..46fc2496da 100644
--- a/packages/misskey-js/src/autogen/types.ts
+++ b/packages/misskey-js/src/autogen/types.ts
@@ -4288,7 +4288,14 @@ export type components = {
       exportedEntity: 'antenna' | 'blocking' | 'clip' | 'customEmoji' | 'favorite' | 'following' | 'muting' | 'note' | 'userList';
       /** Format: id */
       fileId: string;
-    }) | ({
+    }) | {
+      /** Format: id */
+      id: string;
+      /** Format: date-time */
+      createdAt: string;
+      /** @enum {string} */
+      type: 'login';
+    } | ({
       /** Format: id */
       id: string;
       /** Format: date-time */
@@ -18550,8 +18557,8 @@ export type operations = {
           untilId?: string;
           /** @default true */
           markAsRead?: boolean;
-          includeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollEnded' | 'receiveFollowRequest' | 'followRequestAccepted' | 'roleAssigned' | 'achievementEarned' | 'exportCompleted' | 'app' | 'test' | 'pollVote' | 'groupInvited')[];
-          excludeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollEnded' | 'receiveFollowRequest' | 'followRequestAccepted' | 'roleAssigned' | 'achievementEarned' | 'exportCompleted' | 'app' | 'test' | 'pollVote' | 'groupInvited')[];
+          includeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollEnded' | 'receiveFollowRequest' | 'followRequestAccepted' | 'roleAssigned' | 'achievementEarned' | 'exportCompleted' | 'login' | 'app' | 'test' | 'pollVote' | 'groupInvited')[];
+          excludeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollEnded' | 'receiveFollowRequest' | 'followRequestAccepted' | 'roleAssigned' | 'achievementEarned' | 'exportCompleted' | 'login' | 'app' | 'test' | 'pollVote' | 'groupInvited')[];
         };
       };
     };
@@ -18618,8 +18625,8 @@ export type operations = {
           untilId?: string;
           /** @default true */
           markAsRead?: boolean;
-          includeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollEnded' | 'receiveFollowRequest' | 'followRequestAccepted' | 'roleAssigned' | 'achievementEarned' | 'exportCompleted' | 'app' | 'test' | 'reaction:grouped' | 'renote:grouped' | 'pollVote' | 'groupInvited')[];
-          excludeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollEnded' | 'receiveFollowRequest' | 'followRequestAccepted' | 'roleAssigned' | 'achievementEarned' | 'exportCompleted' | 'app' | 'test' | 'reaction:grouped' | 'renote:grouped' | 'pollVote' | 'groupInvited')[];
+          includeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollEnded' | 'receiveFollowRequest' | 'followRequestAccepted' | 'roleAssigned' | 'achievementEarned' | 'exportCompleted' | 'login' | 'app' | 'test' | 'reaction:grouped' | 'renote:grouped' | 'pollVote' | 'groupInvited')[];
+          excludeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollEnded' | 'receiveFollowRequest' | 'followRequestAccepted' | 'roleAssigned' | 'achievementEarned' | 'exportCompleted' | 'login' | 'app' | 'test' | 'reaction:grouped' | 'renote:grouped' | 'pollVote' | 'groupInvited')[];
         };
       };
     };
diff --git a/packages/sw/src/scripts/create-notification.ts b/packages/sw/src/scripts/create-notification.ts
index 2b7dfd4f2d..364328d4b0 100644
--- a/packages/sw/src/scripts/create-notification.ts
+++ b/packages/sw/src/scripts/create-notification.ts
@@ -210,6 +210,12 @@ async function composeNotification(data: PushNotificationDataMap[keyof PushNotif
 						tag: `achievement:${data.body.achievement}`,
 					}];
 
+				case 'login':
+					return [i18n.ts._notification.login, {
+						badge: iconUrl('login-2'),
+						data,
+					}];
+
 				case 'exportCompleted': {
 					const entityName = {
 						antenna: i18n.ts.antennas,
diff --git a/packages/sw/src/types.ts b/packages/sw/src/types.ts
index fac3e707d8..4f82779808 100644
--- a/packages/sw/src/types.ts
+++ b/packages/sw/src/types.ts
@@ -50,4 +50,5 @@ export type BadgeNames =
 	| 'quote'
 	| 'repeat'
 	| 'user-plus'
-	| 'users';
+	| 'users'
+	| 'login-2';

From 9dc058189e0c086d619105017ee03cd879bd4f32 Mon Sep 17 00:00:00 2001
From: Kisaragi <48310258+KisaragiEffective@users.noreply.github.com>
Date: Thu, 3 Oct 2024 15:08:45 +0900
Subject: [PATCH 011/121] =?UTF-8?q?fix(frontend):=20=E3=83=87=E3=83=BC?=
 =?UTF-8?q?=E3=82=BF=E3=82=BB=E3=83=BC=E3=83=90=E3=83=BC=E3=82=92=E6=9C=89?=
 =?UTF-8?q?=E5=8A=B9=E3=81=AB=E3=81=97=E3=81=A6=E3=81=84=E3=82=8B=E3=81=A8?=
 =?UTF-8?q?=E3=81=8D=E3=81=AB=E3=83=A1=E3=83=B3=E3=82=B7=E3=83=A7=E3=83=B3?=
 =?UTF-8?q?=E3=81=AE=E3=82=A2=E3=82=A4=E3=82=B3=E3=83=B3=E3=81=8C=E3=82=A2?=
 =?UTF-8?q?=E3=83=8B=E3=83=A1=E3=83=BC=E3=82=B7=E3=83=A7=E3=83=B3=E3=81=97?=
 =?UTF-8?q?=E3=81=AA=E3=81=84=E3=82=88=E3=81=86=E3=81=AB=20(#14674)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 packages/frontend/src/components/MkMention.vue | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/packages/frontend/src/components/MkMention.vue b/packages/frontend/src/components/MkMention.vue
index 9d9661e816..e809aebe13 100644
--- a/packages/frontend/src/components/MkMention.vue
+++ b/packages/frontend/src/components/MkMention.vue
@@ -41,7 +41,7 @@ const bg = tinycolor(getComputedStyle(document.documentElement).getPropertyValue
 bg.setAlpha(0.1);
 const bgCss = bg.toRgbString();
 
-const avatarUrl = computed(() => defaultStore.state.disableShowingAnimatedImages
+const avatarUrl = computed(() => defaultStore.state.disableShowingAnimatedImages || defaultStore.state.dataSaver.avatar
 	? getStaticImageUrl(`/avatar/@${props.username}@${props.host}`)
 	: `/avatar/@${props.username}@${props.host}`,
 );

From 87617dca39862fded4bd3751e8e2807dd42a32fb Mon Sep 17 00:00:00 2001
From: syuilo <4439005+syuilo@users.noreply.github.com>
Date: Thu, 3 Oct 2024 15:12:07 +0900
Subject: [PATCH 012/121] refactor & performance improvements of MkMention

---
 packages/frontend/src/components/MkMention.vue | 9 +++------
 1 file changed, 3 insertions(+), 6 deletions(-)

diff --git a/packages/frontend/src/components/MkMention.vue b/packages/frontend/src/components/MkMention.vue
index e809aebe13..71bd5addfb 100644
--- a/packages/frontend/src/components/MkMention.vue
+++ b/packages/frontend/src/components/MkMention.vue
@@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 -->
 
 <template>
-<MkA v-user-preview="canonical" :class="[$style.root, { [$style.isMe]: isMe }]" :to="url" :style="{ background: bgCss }" :behavior="navigationBehavior">
+<MkA v-user-preview="canonical" :class="[$style.root, { [$style.isMe]: isMe }]" :to="url" :behavior="navigationBehavior">
 	<img :class="$style.icon" :src="avatarUrl" alt="">
 	<span>
 		<span>@{{ username }}</span>
@@ -16,7 +16,6 @@ SPDX-License-Identifier: AGPL-3.0-only
 <script lang="ts" setup>
 import { toUnicode } from 'punycode';
 import { computed } from 'vue';
-import tinycolor from 'tinycolor2';
 import { host as localHost } from '@@/js/config.js';
 import { $i } from '@/account.js';
 import { defaultStore } from '@/store.js';
@@ -37,10 +36,6 @@ const isMe = $i && (
 	`@${props.username}@${toUnicode(props.host)}` === `@${$i.username}@${toUnicode(localHost)}`.toLowerCase()
 );
 
-const bg = tinycolor(getComputedStyle(document.documentElement).getPropertyValue(isMe ? '--mentionMe' : '--mention'));
-bg.setAlpha(0.1);
-const bgCss = bg.toRgbString();
-
 const avatarUrl = computed(() => defaultStore.state.disableShowingAnimatedImages || defaultStore.state.dataSaver.avatar
 	? getStaticImageUrl(`/avatar/@${props.username}@${props.host}`)
 	: `/avatar/@${props.username}@${props.host}`,
@@ -53,9 +48,11 @@ const avatarUrl = computed(() => defaultStore.state.disableShowingAnimatedImages
 	padding: 4px 8px 4px 4px;
 	border-radius: 999px;
 	color: var(--mention);
+	background: color(from var(--mention) srgb r g b / 0.1);
 
 	&.isMe {
 		color: var(--mentionMe);
+		background: color(from var(--mentionMe) srgb r g b / 0.1);
 	}
 }
 

From e97b7fe2a1e2102aca83fd4aacfc50add9d86b7a Mon Sep 17 00:00:00 2001
From: "github-actions[bot]" <github-actions[bot]@users.noreply.github.com>
Date: Thu, 3 Oct 2024 06:18:15 +0000
Subject: [PATCH 013/121] Bump version to 2024.10.0-alpha.0

---
 CHANGELOG.md                     | 2 +-
 package.json                     | 2 +-
 packages/misskey-js/package.json | 2 +-
 3 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 72c3b22d69..c310bb49a1 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,4 +1,4 @@
-## Unreleased
+## 2024.10.0
 
 ### General
 - Enhance: セキュリティ向上のため、サインイン時もCAPTCHAを求めるようになりました
diff --git a/package.json b/package.json
index edc1d7e318..a463bdb7b8 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
 	"name": "misskey",
-	"version": "2024.9.0",
+	"version": "2024.10.0-alpha.0",
 	"codename": "nasubi",
 	"repository": {
 		"type": "git",
diff --git a/packages/misskey-js/package.json b/packages/misskey-js/package.json
index 684ae381f0..b41f0057a3 100644
--- a/packages/misskey-js/package.json
+++ b/packages/misskey-js/package.json
@@ -1,7 +1,7 @@
 {
 	"type": "module",
 	"name": "misskey-js",
-	"version": "2024.9.0",
+	"version": "2024.10.0-alpha.0",
 	"description": "Misskey SDK for JavaScript",
 	"license": "MIT",
 	"main": "./built/index.js",

From 1e9813e19e2082cb694f20313fd56fcabe616ce8 Mon Sep 17 00:00:00 2001
From: syuilo <4439005+syuilo@users.noreply.github.com>
Date: Thu, 3 Oct 2024 16:16:09 +0900
Subject: [PATCH 014/121] New Crowdin updates (#14649)

* New translations ja-jp.yml (Catalan)

* New translations ja-jp.yml (Catalan)

* New translations ja-jp.yml (English)

* New translations ja-jp.yml (Chinese Simplified)

* New translations ja-jp.yml (Chinese Simplified)

* New translations ja-jp.yml (English)

* New translations ja-jp.yml (Chinese Traditional)

* New translations ja-jp.yml (Korean)

* New translations ja-jp.yml (Chinese Simplified)

* New translations ja-jp.yml (Catalan)

* New translations ja-jp.yml (Italian)

* New translations ja-jp.yml (Catalan)

* New translations ja-jp.yml (Italian)

* New translations ja-jp.yml (Romanian)

* New translations ja-jp.yml (French)

* New translations ja-jp.yml (Spanish)

* New translations ja-jp.yml (Arabic)

* New translations ja-jp.yml (Czech)

* New translations ja-jp.yml (German)

* New translations ja-jp.yml (Greek)

* New translations ja-jp.yml (Hungarian)

* New translations ja-jp.yml (Korean)

* New translations ja-jp.yml (Dutch)

* New translations ja-jp.yml (Norwegian)

* New translations ja-jp.yml (English)

* New translations ja-jp.yml (Portuguese)

* New translations ja-jp.yml (Russian)

* New translations ja-jp.yml (Chinese Traditional)

* New translations ja-jp.yml (Indonesian)

* New translations ja-jp.yml (Polish)

* New translations ja-jp.yml (Slovak)

* New translations ja-jp.yml (Swedish)

* New translations ja-jp.yml (Turkish)

* New translations ja-jp.yml (Ukrainian)

* New translations ja-jp.yml (Chinese Simplified)

* New translations ja-jp.yml (Vietnamese)

* New translations ja-jp.yml (Bengali)

* New translations ja-jp.yml (Thai)

* New translations ja-jp.yml (Uyghur)

* New translations ja-jp.yml (Sinhala)

* New translations ja-jp.yml (Uzbek)

* New translations ja-jp.yml (Kannada)

* New translations ja-jp.yml (Lao)

* New translations ja-jp.yml (Japanese, Kansai)

* New translations ja-jp.yml (Korean (Gyeongsang))
---
 locales/ar-SA.yml |  1 +
 locales/bn-BD.yml |  1 +
 locales/ca-ES.yml | 23 +++++++++++++++++++++++
 locales/cs-CZ.yml |  1 +
 locales/de-DE.yml |  1 +
 locales/el-GR.yml |  1 +
 locales/en-US.yml | 33 ++++++++++++++++++++++++++++++++-
 locales/es-ES.yml |  1 +
 locales/fr-FR.yml |  1 +
 locales/hu-HU.yml |  1 +
 locales/id-ID.yml |  1 +
 locales/it-IT.yml |  7 +++++++
 locales/ja-KS.yml |  1 +
 locales/kn-IN.yml |  2 ++
 locales/ko-GS.yml |  1 +
 locales/ko-KR.yml |  2 ++
 locales/lo-LA.yml |  1 +
 locales/nl-NL.yml |  1 +
 locales/no-NO.yml |  1 +
 locales/pl-PL.yml |  1 +
 locales/pt-PT.yml |  1 +
 locales/ro-RO.yml |  1 +
 locales/ru-RU.yml |  1 +
 locales/si-LK.yml |  3 +++
 locales/sk-SK.yml |  1 +
 locales/sv-SE.yml |  1 +
 locales/th-TH.yml |  1 +
 locales/tr-TR.yml |  1 +
 locales/ug-CN.yml |  3 +++
 locales/uk-UA.yml |  1 +
 locales/uz-UZ.yml |  1 +
 locales/vi-VN.yml |  1 +
 locales/zh-CN.yml | 15 +++++++++++++--
 locales/zh-TW.yml |  2 ++
 34 files changed, 112 insertions(+), 3 deletions(-)

diff --git a/locales/ar-SA.yml b/locales/ar-SA.yml
index b6bfbfa682..24b15ee693 100644
--- a/locales/ar-SA.yml
+++ b/locales/ar-SA.yml
@@ -1533,6 +1533,7 @@ _notification:
     reaction: "التفاعل"
     receiveFollowRequest: "طلبات المتابعة"
     followRequestAccepted: "طلبات المتابعة المقبولة"
+    login: "لِج"
     app: "إشعارات التطبيقات المرتبطة"
   _actions:
     followBack: "تابعك بالمثل"
diff --git a/locales/bn-BD.yml b/locales/bn-BD.yml
index 0d9e4e116c..642fdf2b73 100644
--- a/locales/bn-BD.yml
+++ b/locales/bn-BD.yml
@@ -1313,6 +1313,7 @@ _notification:
     pollEnded: "পোল শেষ"
     receiveFollowRequest: "প্রাপ্ত অনুসরণের অনুরোধসমূহ"
     followRequestAccepted: "গৃহীত অনুসরণের অনুরোধসমূহ"
+    login: "প্রবেশ করুন"
     app: "লিঙ্ক করা অ্যাপ থেকে বিজ্ঞপ্তি"
   _actions:
     followBack: "ফলো ব্যাক করেছে"
diff --git a/locales/ca-ES.yml b/locales/ca-ES.yml
index 9d4ef016ce..bcea736e7a 100644
--- a/locales/ca-ES.yml
+++ b/locales/ca-ES.yml
@@ -236,6 +236,8 @@ silencedInstances: "Instàncies silenciades"
 silencedInstancesDescription: "Llista els enllaços d'amfitrió de les instàncies que vols silenciar. Tots els comptes de les instàncies llistades s'establiran com silenciades i només podran fer sol·licitacions de seguiment, i no podran mencionar als comptes locals si no els segueixen. Això no afectarà les instàncies bloquejades."
 mediaSilencedInstances: "Instàncies amb els arxius silenciats"
 mediaSilencedInstancesDescription: "Llista els noms dels servidors que vulguis silenciar els arxius, un servidor per línia. Tots els comptes que pertanyin als servidors llistats seran tractats com sensibles i no podran fer servir emojis personalitzats. Això no tindrà efecte sobre els servidors blocats."
+federationAllowedHosts: "Llista de servidors federats"
+federationAllowedHostsDescription: "Llista dels servidors amb els quals es federa."
 muteAndBlock: "Silencia i bloca"
 mutedUsers: "Usuaris silenciats"
 blockedUsers: "Usuaris bloquejats"
@@ -334,6 +336,7 @@ renameFolder: "Canvia el nom de la carpeta"
 deleteFolder: "Elimina la carpeta"
 folder: "Carpeta "
 addFile: "Afegeix un fitxer"
+showFile: "Mostrar fitxer"
 emptyDrive: "La teva unitat és buida"
 emptyFolder: "La carpeta està buida"
 unableToDelete: "No es pot eliminar"
@@ -509,6 +512,10 @@ uiLanguage: "Idioma de l'interfície"
 aboutX: "Respecte a {x}"
 emojiStyle: "Estil d'emoji"
 native: "Nadiu"
+menuStyle: "Estil de menú"
+style: "Estil"
+drawer: "Calaix"
+popup: "Emergent"
 showNoteActionsOnlyHover: "Només mostra accions de la nota en passar amb el cursor"
 showReactionsCount: "Mostra el nombre de reaccions a les publicacions"
 noHistory: "No hi ha un registre previ"
@@ -1268,6 +1275,15 @@ fromX: "De {x}"
 genEmbedCode: "Obtenir el codi per incrustar"
 noteOfThisUser: "Notes d'aquest usuari"
 clipNoteLimitExceeded: "No es poden afegir més notes a aquest clip."
+performance: "Rendiment"
+modified: "Modificat"
+discard: "Descarta"
+thereAreNChanges: "Hi ha(n) {n} canvi(s)"
+signinWithPasskey: "Inicia sessió amb Passkey"
+unknownWebAuthnKey: "Passkey desconeguda"
+passkeyVerificationFailed: "La verificació a fallat"
+passkeyVerificationSucceededButPasswordlessLoginDisabled: "La verificació de la passkey a estat correcta, però s'ha deshabilitat l'inici de sessió sense contrasenya."
+messageToFollower: "Missatge als meus seguidors"
 _delivery:
   status: "Estat d'entrega "
   stop: "Suspés"
@@ -2235,6 +2251,9 @@ _profile:
   changeBanner: "Canviar el bàner "
   verifiedLinkDescription: "Escrivint una adreça URL que enllaci a aquest perfil, una icona de propietat verificada es mostrarà al costat del camp."
   avatarDecorationMax: "Pot afegir un màxim de {max} decoracions."
+  followedMessage: "Missatge als nous seguidors"
+  followedMessageDescription: "Es pot configurar un missatge curt que es mostra a l'altra persona quan comença a seguir-te."
+  followedMessageDescriptionForLockedAccount: "Si comencen a seguir-te es mostra un missatge de quan es permet aquesta sol·licitud. "
 _exportOrImport:
   allNotes: "Totes les publicacions"
   favoritedNotes: "Notes preferides"
@@ -2373,6 +2392,7 @@ _notification:
   renotedBySomeUsers: "L'han impulsat {n} usuaris"
   followedBySomeUsers: "Et segueixen {n} usuaris"
   flushNotification: "Netejar notificacions"
+  exportOfXCompleted: "Completada l'exportació de {n}"
   _types:
     all: "Tots"
     note: "Notes noves"
@@ -2387,6 +2407,9 @@ _notification:
     followRequestAccepted: "Petició de seguiment acceptada"
     roleAssigned: "Rol donat"
     achievementEarned: "Assoliment desbloquejat"
+    exportCompleted: "Exportació completada"
+    login: "Iniciar sessió"
+    test: "Prova la notificació"
     app: "Notificacions d'aplicacions"
   _actions:
     followBack: "t'ha seguit també"
diff --git a/locales/cs-CZ.yml b/locales/cs-CZ.yml
index 4a27ed7635..1e391fcc31 100644
--- a/locales/cs-CZ.yml
+++ b/locales/cs-CZ.yml
@@ -1962,6 +1962,7 @@ _notification:
     receiveFollowRequest: "Obdržené žádosti o sledování"
     followRequestAccepted: "Přijaté žádosti o sledování"
     achievementEarned: "Úspěch odemčen"
+    login: "Přihlásit se"
     app: "Oznámení z propojených aplikací"
   _actions:
     followBack: "vás začal sledovat zpět"
diff --git a/locales/de-DE.yml b/locales/de-DE.yml
index 453f6308f6..871ed87564 100644
--- a/locales/de-DE.yml
+++ b/locales/de-DE.yml
@@ -2141,6 +2141,7 @@ _notification:
     receiveFollowRequest: "Erhaltene Follow-Anfragen"
     followRequestAccepted: "Akzeptierte Follow-Anfragen"
     achievementEarned: "Errungenschaft freigeschaltet"
+    login: "Anmelden"
     app: "Benachrichtigungen von Apps"
   _actions:
     followBack: "folgt dir nun auch"
diff --git a/locales/el-GR.yml b/locales/el-GR.yml
index 5eca348e18..4657842ca5 100644
--- a/locales/el-GR.yml
+++ b/locales/el-GR.yml
@@ -378,6 +378,7 @@ _notification:
     renote: "Κοινοποίηση σημειώματος"
     quote: "Παράθεση"
     reaction: "Αντιδράσεις"
+    login: "Σύνδεση"
   _actions:
     reply: "Απάντηση"
     renote: "Κοινοποίηση σημειώματος"
diff --git a/locales/en-US.yml b/locales/en-US.yml
index ad81376f89..7db3315f7d 100644
--- a/locales/en-US.yml
+++ b/locales/en-US.yml
@@ -236,6 +236,8 @@ silencedInstances: "Silenced instances"
 silencedInstancesDescription: "List the host names of the servers that you want to silence, separated by a new line. All accounts belonging to the listed servers will be treated as silenced, and can only make follow requests, and cannot mention local accounts if not followed. This will not affect the blocked servers."
 mediaSilencedInstances: "Media-silenced servers"
 mediaSilencedInstancesDescription: "List the host names of the servers that you want to media-silence, separated by a new line. All accounts belonging to the listed servers will be treated as sensitive, and can't use custom emojis. This will not affect the blocked servers."
+federationAllowedHosts: "Federation allowed servers"
+federationAllowedHostsDescription: "Specify the hostnames of the servers you want to allow federation separated by line breaks."
 muteAndBlock: "Mutes and Blocks"
 mutedUsers: "Muted users"
 blockedUsers: "Blocked users"
@@ -334,6 +336,7 @@ renameFolder: "Rename this folder"
 deleteFolder: "Delete this folder"
 folder: "Folder"
 addFile: "Add a file"
+showFile: "Show files"
 emptyDrive: "Your Drive is empty"
 emptyFolder: "This folder is empty"
 unableToDelete: "Unable to delete"
@@ -509,6 +512,10 @@ uiLanguage: "User interface language"
 aboutX: "About {x}"
 emojiStyle: "Emoji style"
 native: "Native"
+menuStyle: "Menu style"
+style: "Style"
+drawer: "Drawer"
+popup: "Pop up"
 showNoteActionsOnlyHover: "Only show note actions on hover"
 showReactionsCount: "See the number of reactions in notes"
 noHistory: "No history available"
@@ -591,6 +598,8 @@ ascendingOrder: "Ascending"
 descendingOrder: "Descending"
 scratchpad: "Scratchpad"
 scratchpadDescription: "The Scratchpad provides an environment for AiScript experiments. You can write, execute, and check the results of it interacting with Misskey in it."
+uiInspector: "UI inspector"
+uiInspectorDescription: "You can see the UI component server list on memory. UI component will be generated by Ui:C: function."
 output: "Output"
 script: "Script"
 disablePagesScript: "Disable AiScript on Pages"
@@ -1125,7 +1134,7 @@ options: "Options"
 specifyUser: "Specific user"
 lookupConfirm: "Do you want to look up?"
 openTagPageConfirm: "Do you want to open a hashtag page?"
-specifyHost: "Specify a host"
+specifyHost: "Specific host"
 failedToPreviewUrl: "Could not preview"
 update: "Update"
 rolesThatCanBeUsedThisEmojiAsReaction: "Roles that can use this emoji as reaction"
@@ -1266,6 +1275,14 @@ fromX: "From {x}"
 genEmbedCode: "Generate embed code"
 noteOfThisUser: "Notes by this user"
 clipNoteLimitExceeded: "No more notes can be added to this clip."
+performance: "Performance"
+modified: "Modified"
+discard: "Discard"
+thereAreNChanges: "There are {n} change(s)"
+signinWithPasskey: "Sign in with Passkey"
+unknownWebAuthnKey: "Unknown Passkey"
+passkeyVerificationFailed: "Passkey verification has failed."
+passkeyVerificationSucceededButPasswordlessLoginDisabled: "Passkey verification has succeeded but password-less login is disabled."
 _delivery:
   status: "Delivery status"
   stop: "Suspended"
@@ -1400,6 +1417,7 @@ _serverSettings:
   fanoutTimelineDescription: "Greatly increases performance of timeline retrieval and reduces load on the database when enabled. In exchange, memory usage of Redis will increase. Consider disabling this in case of low server memory or server instability."
   fanoutTimelineDbFallback: "Fallback to database"
   fanoutTimelineDbFallbackDescription: "When enabled, the timeline will fall back to the database for additional queries if the timeline is not cached. Disabling it further reduces the server load by eliminating the fallback process, but limits the range of timelines that can be retrieved."
+  reactionsBufferingDescription: "When enabled, performance during reaction creation will be greatly improved, reducing the load on the database. However, Redis memory usage will increase."
   inquiryUrl: "Inquiry URL"
   inquiryUrlDescription: "Specify a URL for the inquiry form to the server maintainer or a web page for the contact information."
 _accountMigration:
@@ -1733,6 +1751,11 @@ _role:
     canSearchNotes: "Usage of note search"
     canUseTranslator: "Translator usage"
     avatarDecorationLimit: "Maximum number of avatar decorations that can be applied"
+    canImportAntennas: "Allow importing antennas"
+    canImportBlocking: "Allow importing blocking"
+    canImportFollowing: "Allow importing following"
+    canImportMuting: "Allow importing muting"
+    canImportUserLists: "Allow importing lists"
   _condition:
     roleAssignedTo: "Assigned to manual roles"
     isLocal: "Local user"
@@ -2227,6 +2250,9 @@ _profile:
   changeBanner: "Change banner"
   verifiedLinkDescription: "By entering an URL that contains a link to your profile here, an ownership verification icon can be displayed next to the field."
   avatarDecorationMax: "You can add up to {max} decorations."
+  followedMessage: "Message when you are followed"
+  followedMessageDescription: "You can set a short message to be displayed to the recipient when they follow you."
+  followedMessageDescriptionForLockedAccount: "If you have set up that follow requests require approval, this will be displayed when you grant a follow request."
 _exportOrImport:
   allNotes: "All notes"
   favoritedNotes: "Favorite notes"
@@ -2365,6 +2391,7 @@ _notification:
   renotedBySomeUsers: "Renote from {n} users"
   followedBySomeUsers: "Followed by {n} users"
   flushNotification: "Clear notifications"
+  exportOfXCompleted: "Export of {x} has been completed"
   _types:
     all: "All"
     note: "New notes"
@@ -2379,6 +2406,9 @@ _notification:
     followRequestAccepted: "Accepted follow requests"
     roleAssigned: "Role given"
     achievementEarned: "Achievement unlocked"
+    exportCompleted: "The export has been completed"
+    login: "Sign In"
+    test: "Notification test"
     app: "Notifications from linked apps"
   _actions:
     followBack: "followed you back"
@@ -2445,6 +2475,7 @@ _webhookSettings:
     abuseReportResolved: "When resolved abuse report"
     userCreated: "When user is created"
   deleteConfirm: "Are you sure you want to delete the Webhook?"
+  testRemarks: "Click the button to the right of the switch to send a test Webhook with dummy data."
 _abuseReport:
   _notificationRecipient:
     createRecipient: "Add a recipient for abuse reports"
diff --git a/locales/es-ES.yml b/locales/es-ES.yml
index 66cab3e957..10966a77b6 100644
--- a/locales/es-ES.yml
+++ b/locales/es-ES.yml
@@ -2343,6 +2343,7 @@ _notification:
     followRequestAccepted: "El seguimiento fue aceptado"
     roleAssigned: "Rol asignado"
     achievementEarned: "Logro desbloqueado"
+    login: "Iniciar sesión"
     app: "Notificaciones desde aplicaciones"
   _actions:
     followBack: "Te sigue de vuelta"
diff --git a/locales/fr-FR.yml b/locales/fr-FR.yml
index 0cf4b65c38..d15fadcb1c 100644
--- a/locales/fr-FR.yml
+++ b/locales/fr-FR.yml
@@ -2037,6 +2037,7 @@ _notification:
     followRequestAccepted: "Demande d'abonnement acceptée"
     roleAssigned: "Rôle reçu"
     achievementEarned: "Déverrouillage d'accomplissement"
+    login: "Se connecter"
     app: "Notifications provenant des apps"
   _actions:
     followBack: "Suivre"
diff --git a/locales/hu-HU.yml b/locales/hu-HU.yml
index 023a91494d..acc27ed092 100644
--- a/locales/hu-HU.yml
+++ b/locales/hu-HU.yml
@@ -96,6 +96,7 @@ _notification:
     renote: "Renote"
     quote: "Idézet"
     reaction: "Reakciók"
+    login: "Bejelentkezés"
   _actions:
     renote: "Renote"
 _deck:
diff --git a/locales/id-ID.yml b/locales/id-ID.yml
index 55ca9d91ac..4c2040dd07 100644
--- a/locales/id-ID.yml
+++ b/locales/id-ID.yml
@@ -2354,6 +2354,7 @@ _notification:
     followRequestAccepted: "Permintaan mengikuti disetujui"
     roleAssigned: "Peran Diberikan"
     achievementEarned: "Pencapaian didapatkan"
+    login: "Masuk"
     app: "Notifikasi dari aplikasi tertaut"
   _actions:
     followBack: "Ikuti Kembali"
diff --git a/locales/it-IT.yml b/locales/it-IT.yml
index 55b612cac5..0399ba4d9c 100644
--- a/locales/it-IT.yml
+++ b/locales/it-IT.yml
@@ -236,6 +236,8 @@ silencedInstances: "Istanze silenziate"
 silencedInstancesDescription: "Elenca i nomi host delle istanze che vuoi silenziare. Tutti i profili nelle istanze silenziate vengono trattati come tali. Possono solo inviare richieste di follow e menzionare soltanto i profili locali che seguono. Le istanze bloccate non sono interessate."
 mediaSilencedInstances: "Istanze coi media silenziati"
 mediaSilencedInstancesDescription: "Elenca i nomi host delle istanze di cui vuoi silenziare i media, uno per riga. Tutti gli allegati dei profili nelle istanze silenziate per via degli allegati espliciti, verranno impostati come tali, le emoji personalizzate non saranno disponibili. Le istanze bloccate sono escluse."
+federationAllowedHosts: "Server a cui consentire la federazione"
+federationAllowedHostsDescription: "Indica gli host dei server a cui è consentita la federazione, uno per ogni linea."
 muteAndBlock: "Silenziare e bloccare"
 mutedUsers: "Profili silenziati"
 blockedUsers: "Profili bloccati"
@@ -1281,6 +1283,7 @@ signinWithPasskey: "Accedi con passkey"
 unknownWebAuthnKey: "Questa è una passkey sconosciuta."
 passkeyVerificationFailed: "La verifica della passkey non è riuscita."
 passkeyVerificationSucceededButPasswordlessLoginDisabled: "La verifica della passkey è riuscita, ma l'accesso senza password è disabilitato."
+messageToFollower: "Messaggio ai follower"
 _delivery:
   status: "Stato della consegna"
   stop: "Sospensione"
@@ -2248,6 +2251,9 @@ _profile:
   changeBanner: "Cambia intestazione"
   verifiedLinkDescription: "Puoi verificare il tuo profilo mostrando una icona. Devi inserire la URL alla pagina che contiene un link al tuo profilo."
   avatarDecorationMax: "Puoi aggiungere fino a {max} decorazioni."
+  followedMessage: "Messaggio, quando qualcuno ti segue"
+  followedMessageDescription: "Puoi impostare un breve messaggio da mostrare agli altri profili quando ti seguono."
+  followedMessageDescriptionForLockedAccount: "Quando approvi una richiesta di follow, verrà visualizzato questo testo."
 _exportOrImport:
   allNotes: "Tutte le note"
   favoritedNotes: "Note preferite"
@@ -2402,6 +2408,7 @@ _notification:
     roleAssigned: "Ruolo concesso"
     achievementEarned: "Risultato raggiunto"
     exportCompleted: "Esportazione completata"
+    login: "Accedi"
     test: "Prova la notifica"
     app: "Notifiche da applicazioni"
   _actions:
diff --git a/locales/ja-KS.yml b/locales/ja-KS.yml
index 660fa38e38..4f950059a7 100644
--- a/locales/ja-KS.yml
+++ b/locales/ja-KS.yml
@@ -2374,6 +2374,7 @@ _notification:
     followRequestAccepted: "フォローが受理されたで"
     roleAssigned: "ロールが付与された"
     achievementEarned: "実績の獲得"
+    login: "ログイン"
     app: "連携アプリからの通知や"
   _actions:
     followBack: "フォローバック"
diff --git a/locales/kn-IN.yml b/locales/kn-IN.yml
index b3ad46f2b1..222599572a 100644
--- a/locales/kn-IN.yml
+++ b/locales/kn-IN.yml
@@ -77,6 +77,8 @@ _profile:
   username: "ಬಳಕೆಹೆಸರು"
 _notification:
   youWereFollowed: "ಹಿಂಬಾಲಿಸಿದರು"
+  _types:
+    login: "ಪ್ರವೇಶ"
   _actions:
     reply: "ಉತ್ತರಿಸು"
 _deck:
diff --git a/locales/ko-GS.yml b/locales/ko-GS.yml
index 082140f2e9..f8a0d328a3 100644
--- a/locales/ko-GS.yml
+++ b/locales/ko-GS.yml
@@ -813,6 +813,7 @@ _notification:
     mention: "멘션"
     quote: "따오기"
     reaction: "반엉"
+    login: "로그인"
   _actions:
     reply: "답하기"
 _deck:
diff --git a/locales/ko-KR.yml b/locales/ko-KR.yml
index f737a74d5d..76ad982056 100644
--- a/locales/ko-KR.yml
+++ b/locales/ko-KR.yml
@@ -1283,6 +1283,7 @@ signinWithPasskey: "패스키로 로그인"
 unknownWebAuthnKey: "등록되지 않은 패스키입니다."
 passkeyVerificationFailed: "패스키 검증을 실패했습니다."
 passkeyVerificationSucceededButPasswordlessLoginDisabled: "패스키를 검증했으나, 비밀번호 없이 로그인하기가 꺼져 있습니다."
+messageToFollower: "팔로워에 보낼 메시지"
 _delivery:
   status: "전송 상태"
   stop: "정지됨"
@@ -2407,6 +2408,7 @@ _notification:
     roleAssigned: "역할이 부여 됨"
     achievementEarned: "도전 과제 획득"
     exportCompleted: "추출을 성공함"
+    login: "로그인"
     test: "알림 테스트"
     app: "연동된 앱을 통한 알림"
   _actions:
diff --git a/locales/lo-LA.yml b/locales/lo-LA.yml
index 1bead5635d..b100d0300f 100644
--- a/locales/lo-LA.yml
+++ b/locales/lo-LA.yml
@@ -456,6 +456,7 @@ _notification:
     renote: "Renote"
     quote: "ອ້າງອີງ"
     reaction: "Reaction"
+    login: "ເຂົ້າ​ສູ່​ລະ​ບົບ"
   _actions:
     reply: "ຕອບ​ກັບ"
     renote: "Renote"
diff --git a/locales/nl-NL.yml b/locales/nl-NL.yml
index eb48cf72da..dde3035357 100644
--- a/locales/nl-NL.yml
+++ b/locales/nl-NL.yml
@@ -486,6 +486,7 @@ _notification:
     renote: "Herdelen"
     quote: "Quote"
     reaction: "Reacties"
+    login: "Inloggen"
   _actions:
     reply: "Antwoord"
     renote: "Herdelen"
diff --git a/locales/no-NO.yml b/locales/no-NO.yml
index cd00ecf9ab..c5f61db745 100644
--- a/locales/no-NO.yml
+++ b/locales/no-NO.yml
@@ -701,6 +701,7 @@ _notification:
     renote: "Renotes"
     quote: "Sitater"
     reaction: "Reaksjoner"
+    login: "Logg inn"
   _actions:
     reply: "Svar"
     renote: "Renote"
diff --git a/locales/pl-PL.yml b/locales/pl-PL.yml
index f586ff2bff..0073628673 100644
--- a/locales/pl-PL.yml
+++ b/locales/pl-PL.yml
@@ -1509,6 +1509,7 @@ _notification:
     reaction: "Reakcja"
     receiveFollowRequest: "Otrzymano prośbę o możliwość obserwacji"
     followRequestAccepted: "Przyjęto prośbę o możliwość obserwacji"
+    login: "Zaloguj się"
     app: "Powiadomienia z aplikacji"
   _actions:
     followBack: "zaobserwował cię z powrotem"
diff --git a/locales/pt-PT.yml b/locales/pt-PT.yml
index 34de5066f3..f5d29891df 100644
--- a/locales/pt-PT.yml
+++ b/locales/pt-PT.yml
@@ -2376,6 +2376,7 @@ _notification:
     followRequestAccepted: "Aceitou pedidos de seguidor"
     roleAssigned: "Cargo dado"
     achievementEarned: "Conquista desbloqueada"
+    login: "Iniciar sessão"
     app: "Notificações de aplicativos conectados"
   _actions:
     followBack: "te seguiu de volta"
diff --git a/locales/ro-RO.yml b/locales/ro-RO.yml
index a5f8057860..88495a41a1 100644
--- a/locales/ro-RO.yml
+++ b/locales/ro-RO.yml
@@ -714,6 +714,7 @@ _notification:
     renote: "Re-notează"
     quote: "Citează"
     reaction: "Reacție"
+    login: "Autentifică-te"
   _actions:
     reply: "Răspunde"
     renote: "Re-notează"
diff --git a/locales/ru-RU.yml b/locales/ru-RU.yml
index cdc4898a3b..15e33c7f4d 100644
--- a/locales/ru-RU.yml
+++ b/locales/ru-RU.yml
@@ -2046,6 +2046,7 @@ _notification:
     receiveFollowRequest: "Получен запрос на подписку"
     followRequestAccepted: "Запрос на подписку одобрен"
     achievementEarned: "Получение достижений"
+    login: "Войти"
     app: "Уведомления из приложений"
   _actions:
     followBack: "отвечает взаимной подпиской"
diff --git a/locales/si-LK.yml b/locales/si-LK.yml
index e130d68ed8..c43f3d860d 100644
--- a/locales/si-LK.yml
+++ b/locales/si-LK.yml
@@ -17,3 +17,6 @@ _sfx:
   note: "නෝට්"
 _profile:
   username: "පරිශීලක නාමය"
+_notification:
+  _types:
+    login: "පිවිසෙන්න"
diff --git a/locales/sk-SK.yml b/locales/sk-SK.yml
index eb1675bdb0..ad004eb4e2 100644
--- a/locales/sk-SK.yml
+++ b/locales/sk-SK.yml
@@ -1409,6 +1409,7 @@ _notification:
     pollEnded: "Hlasovanie skončilo"
     receiveFollowRequest: "Doručené žiadosti o sledovanie"
     followRequestAccepted: "Schválené žiadosti o sledovanie"
+    login: "Prihlásiť sa"
     app: "Oznámenia z prepojených aplikácií"
   _actions:
     followBack: "Sledovať späť\n"
diff --git a/locales/sv-SE.yml b/locales/sv-SE.yml
index c1a998b8fb..5a0de660e8 100644
--- a/locales/sv-SE.yml
+++ b/locales/sv-SE.yml
@@ -562,6 +562,7 @@ _notification:
     renote: "Omnotera"
     quote: "Citat"
     reaction: "Reaktioner"
+    login: "Logga in"
   _actions:
     reply: "Svara"
     renote: "Omnotera"
diff --git a/locales/th-TH.yml b/locales/th-TH.yml
index f5d29a2ce5..77fea6a68e 100644
--- a/locales/th-TH.yml
+++ b/locales/th-TH.yml
@@ -2374,6 +2374,7 @@ _notification:
     followRequestAccepted: "อนุมัติให้ติดตามแล้ว"
     roleAssigned: "ให้บทบาท"
     achievementEarned: "ปลดล็อกความสำเร็จแล้ว"
+    login: "เข้าสู่ระบบ"
     app: "การแจ้งเตือนจากแอปที่มีลิงก์"
   _actions:
     followBack: "ติดตามกลับด้วย"
diff --git a/locales/tr-TR.yml b/locales/tr-TR.yml
index cf6729a81d..fe2f158ff6 100644
--- a/locales/tr-TR.yml
+++ b/locales/tr-TR.yml
@@ -446,6 +446,7 @@ _notification:
     reaction: "Tepkiler"
     receiveFollowRequest: "Takip isteği alındı"
     followRequestAccepted: "Takip isteği kabul edildi"
+    login: "Giriş Yap "
   _actions:
     reply: "yanıt"
     renote: "vazgeçme"
diff --git a/locales/ug-CN.yml b/locales/ug-CN.yml
index e48f64511c..fef26040a5 100644
--- a/locales/ug-CN.yml
+++ b/locales/ug-CN.yml
@@ -17,3 +17,6 @@ _2fa:
   renewTOTPCancel: "ئۇنى توختىتىڭ"
 _widgets:
   profile: "profile"
+_notification:
+  _types:
+    login: "كىرىش"
diff --git a/locales/uk-UA.yml b/locales/uk-UA.yml
index e51156ce22..ef01c8186c 100644
--- a/locales/uk-UA.yml
+++ b/locales/uk-UA.yml
@@ -1587,6 +1587,7 @@ _notification:
     reaction: "Реакції"
     receiveFollowRequest: "Запити на підписку"
     followRequestAccepted: "Прийняті підписки"
+    login: "Увійти"
     app: "Сповіщення від додатків"
   _actions:
     reply: "Відповісти"
diff --git a/locales/uz-UZ.yml b/locales/uz-UZ.yml
index cf2e5f2fe7..7c5d2796f6 100644
--- a/locales/uz-UZ.yml
+++ b/locales/uz-UZ.yml
@@ -1057,6 +1057,7 @@ _notification:
     quote: "Iqtibos keltirish"
     reaction: "Reaktsiyalar"
     receiveFollowRequest: "Qabul qilingan kuzatuv so'rovlari"
+    login: "Kirish"
   _actions:
     reply: "Javob berish"
     renote: "Qayta qayd qilish"
diff --git a/locales/vi-VN.yml b/locales/vi-VN.yml
index f3979bbd3c..c84eb574f3 100644
--- a/locales/vi-VN.yml
+++ b/locales/vi-VN.yml
@@ -1878,6 +1878,7 @@ _notification:
     receiveFollowRequest: "Yêu cầu theo dõi"
     followRequestAccepted: "Yêu cầu theo dõi được chấp nhận"
     achievementEarned: "Hoàn thành Achievement"
+    login: "Đăng nhập"
     app: "Từ app liên kết"
   _actions:
     followBack: "đã theo dõi lại bạn"
diff --git a/locales/zh-CN.yml b/locales/zh-CN.yml
index 0d76361d6f..7b2037d076 100644
--- a/locales/zh-CN.yml
+++ b/locales/zh-CN.yml
@@ -90,7 +90,7 @@ followsYou: "正在关注你"
 createList: "创建列表"
 manageLists: "管理列表"
 error: "错误"
-somethingHappened: "出现了一些问题!"
+somethingHappened: "出错了"
 retry: "重试"
 pageLoadError: "页面加载失败。"
 pageLoadErrorDescription: "这通常是由于网络或浏览器缓存的原因。请清除缓存或等待片刻后重试。"
@@ -167,7 +167,7 @@ emojiUrl: "emoji 地址"
 addEmoji: "添加表情符号"
 settingGuide: "推荐配置"
 cacheRemoteFiles: "缓存远程文件"
-cacheRemoteFilesDescription: "启用此设定时,将在此服务器上缓存远程文件。虽然可以加快图片显示的速度,但是相对的会消耗大量的服务器存储空间。用户角色内的网盘容量决定了这个远程用户能在服务器上保留保留多少缓存。当超出了这个限制时,旧的文件将从缓存中被删除,成为链接。当禁用此设定时,则是从一开始就将远程文件保留为链接。此时推荐将 default.yml 的 proxyRemoteFiles 设置为 true 以优化缩略图生成及保护用户隐私。"
+cacheRemoteFilesDescription: "启用此设定时,将在此服务器上缓存远程文件。虽然可以加快图片显示的速度,但是相对的会消耗大量的服务器存储空间。用户角色内的网盘容量决定了这个远程用户能在服务器上保留多少缓存。当超出了这个限制时,旧的文件将从缓存中被删除,成为链接。当禁用此设定时,则是从一开始就将远程文件保留为链接。此时推荐将 default.yml 的 proxyRemoteFiles 设置为 true 以优化缩略图生成及保护用户隐私。"
 youCanCleanRemoteFilesCache: "可以使用文件管理的🗑️按钮来删除所有的缓存。"
 cacheRemoteSensitiveFiles: "缓存远程敏感媒体文件"
 cacheRemoteSensitiveFilesDescription: "如果禁用这项设定,远程服务器的敏感媒体将不会被缓存,而是直接链接。"
@@ -236,6 +236,8 @@ silencedInstances: "被静音的服务器"
 silencedInstancesDescription: "设置要静音的服务器,以换行分隔。被静音的服务器内所有的账户将默认处于「静音」状态,仅能发送关注请求,并且在未关注状态下无法提及本地账户。被阻止的实例不受影响。"
 mediaSilencedInstances: "已隐藏媒体文件的服务器"
 mediaSilencedInstancesDescription: "设置要隐藏媒体文件的服务器,以换行分隔。被设置为隐藏媒体文件服务器内所有账号的文件均按照「敏感内容」处理,且将无法使用自定义表情符号。被阻止的实例不受影响。"
+federationAllowedHosts: "允许联合的服务器"
+federationAllowedHostsDescription: "设定允许联合的服务器,以换行分隔。"
 muteAndBlock: "静音/拉黑"
 mutedUsers: "已静音用户"
 blockedUsers: "已拉黑的用户"
@@ -512,6 +514,7 @@ emojiStyle: "表情符号的样式"
 native: "原生"
 menuStyle: "菜单样式"
 style: "样式"
+drawer: "抽屉"
 popup: "弹窗"
 showNoteActionsOnlyHover: "仅在悬停时显示帖子操作"
 showReactionsCount: "显示帖子的回应数"
@@ -1273,10 +1276,14 @@ genEmbedCode: "生成嵌入代码"
 noteOfThisUser: "此用户的帖子"
 clipNoteLimitExceeded: "无法再往此便签内添加更多帖子"
 performance: "性能"
+modified: "有变更"
+discard: "取消"
+thereAreNChanges: "有 {n} 处更改"
 signinWithPasskey: "使用通行密钥登录"
 unknownWebAuthnKey: "此通行密钥未注册。"
 passkeyVerificationFailed: "验证通行密钥失败。"
 passkeyVerificationSucceededButPasswordlessLoginDisabled: "通行密钥验证成功,但账户未开启无密码登录。"
+messageToFollower: "给关注者的消息"
 _delivery:
   status: "投递状态"
   stop: "停止投递"
@@ -2244,6 +2251,9 @@ _profile:
   changeBanner: "修改横幅"
   verifiedLinkDescription: "如果将内容设置为 URL,当链接所指向的网页内包含自己的个人资料链接时,可以显示一个已验证图标。"
   avatarDecorationMax: "最多可添加 {max} 个挂件"
+  followedMessage: "被关注时显示的消息"
+  followedMessageDescription: "可以设置被关注时向对方显示的短消息。"
+  followedMessageDescriptionForLockedAccount: "需要批准才能关注的情况下,消息是在被请求被批准后显示。"
 _exportOrImport:
   allNotes: "所有帖子"
   favoritedNotes: "收藏的帖子"
@@ -2398,6 +2408,7 @@ _notification:
     roleAssigned: "授予的角色"
     achievementEarned: "取得的成就"
     exportCompleted: "已完成导出"
+    login: "登录"
     test: "测试通知"
     app: "关联应用的通知"
   _actions:
diff --git a/locales/zh-TW.yml b/locales/zh-TW.yml
index 74c03befd1..f73bba6664 100644
--- a/locales/zh-TW.yml
+++ b/locales/zh-TW.yml
@@ -1283,6 +1283,7 @@ signinWithPasskey: "使用密碼金鑰登入"
 unknownWebAuthnKey: "未註冊的金鑰。"
 passkeyVerificationFailed: "驗證金鑰失敗。"
 passkeyVerificationSucceededButPasswordlessLoginDisabled: "雖然驗證金鑰成功,但是無密碼登入的方式是停用的。"
+messageToFollower: "給追隨者的訊息"
 _delivery:
   status: "傳送狀態"
   stop: "停止發送"
@@ -2407,6 +2408,7 @@ _notification:
     roleAssigned: "已授予角色"
     achievementEarned: "獲得成就"
     exportCompleted: "已完成匯出。"
+    login: "登入"
     test: "通知測試"
     app: "應用程式通知"
   _actions:

From a722ea8ccd98c66784442d71a1e1cd14b7835d48 Mon Sep 17 00:00:00 2001
From: Kisaragi <48310258+KisaragiEffective@users.noreply.github.com>
Date: Thu, 3 Oct 2024 17:05:14 +0900
Subject: [PATCH 015/121] =?UTF-8?q?fix(backend):=20=E9=80=A3=E5=90=88?=
 =?UTF-8?q?=E9=99=90=E5=AE=9A=E5=85=88=E3=81=8C=E9=96=93=E9=81=95=E3=81=A3?=
 =?UTF-8?q?=E3=81=A6=E9=80=A3=E5=90=88=E3=81=97=E3=81=AA=E3=81=84=E5=85=88?=
 =?UTF-8?q?=E3=81=AB=E4=BB=A3=E5=85=A5=E3=81=95=E3=82=8C=E3=81=A6=E3=81=84?=
 =?UTF-8?q?=E3=82=8B=E3=81=AE=E3=82=92=E4=BF=AE=E6=AD=A3=20(#14662)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

* fix(backend): 連合限定先が間違って連合しない先に代入されているのを修正

* build: fix property typo
---
 packages/backend/src/server/api/endpoints/admin/update-meta.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

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 daef236397..9ffae840b6 100644
--- a/packages/backend/src/server/api/endpoints/admin/update-meta.ts
+++ b/packages/backend/src/server/api/endpoints/admin/update-meta.ts
@@ -652,7 +652,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 			}
 
 			if (Array.isArray(ps.federationHosts)) {
-				set.blockedHosts = ps.federationHosts.filter(Boolean).map(x => x.toLowerCase());
+				set.federationHosts = ps.federationHosts.filter(Boolean).map(x => x.toLowerCase());
 			}
 
 			const before = await this.metaService.fetch(true);

From a09b03ed3ae6f137d5a11862222222d6a4b172d8 Mon Sep 17 00:00:00 2001
From: syuilo <4439005+syuilo@users.noreply.github.com>
Date: Thu, 3 Oct 2024 17:06:16 +0900
Subject: [PATCH 016/121] Update CHANGELOG.md

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

diff --git a/CHANGELOG.md b/CHANGELOG.md
index c310bb49a1..0aa9ecaac7 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,7 @@
 
 ### General
 - Enhance: セキュリティ向上のため、サインイン時もCAPTCHAを求めるようになりました
+- Fix: 連合のホワイトリストが正常に登録されない問題を修正
 
 ### Client
 - Enhance: フォロワーへのメッセージ欄のデザイン改良

From 75ea9643120651e1571ba211680a866e6c45152b Mon Sep 17 00:00:00 2001
From: syuilo <4439005+syuilo@users.noreply.github.com>
Date: Thu, 3 Oct 2024 17:07:16 +0900
Subject: [PATCH 017/121] Update CHANGELOG.md

---
 CHANGELOG.md | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0aa9ecaac7..188e3b7d82 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,10 +2,12 @@
 
 ### General
 - Enhance: セキュリティ向上のため、サインイン時もCAPTCHAを求めるようになりました
+- Enhance: 依存関係の更新
+- Enhance: l10nの更新
 - Fix: 連合のホワイトリストが正常に登録されない問題を修正
 
 ### Client
-- Enhance: フォロワーへのメッセージ欄のデザイン改良
+- Enhance: デザインの調整
 
 ### Server
 - Enhance: セキュリティ向上のため、ログイン時にメール通知を行うように

From 2c1a7470d35cb840950e63008fb4014e5e341dd6 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=8B=E3=81=A3=E3=81=93=E3=81=8B=E3=82=8A?=
 <67428053+kakkokari-gtyih@users.noreply.github.com>
Date: Thu, 3 Oct 2024 18:18:00 +0900
Subject: [PATCH 018/121] =?UTF-8?q?feat:=20=E3=82=B5=E3=83=BC=E3=83=90?=
 =?UTF-8?q?=E3=83=BC=E5=88=9D=E6=9C=9F=E8=A8=AD=E5=AE=9A=E6=99=82=E3=81=AB?=
 =?UTF-8?q?=E5=88=9D=E6=9C=9F=E3=83=91=E3=82=B9=E3=83=AF=E3=83=BC=E3=83=89?=
 =?UTF-8?q?=E3=82=92=E8=A6=81=E6=B1=82=E3=81=A7=E3=81=8D=E3=82=8B=E3=82=88?=
 =?UTF-8?q?=E3=81=86=E3=81=AB=20(#14626)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

* feat: サーバー初期設定時専用の初期パスワードを設定できるように

* 無いのに入力された場合もエラーにする

* :art:

* :art:

* cypress-devcontainerにもpassを設定(テストが失敗するため)

* [ci skip] :art:

* :v:

* test: please revert this commit before merge

* Revert "test: please revert this commit before merge"

This reverts commit 66b2b48f66830d2450d8cda03955c143feba76c7.

* Update locales/ja-JP.yml

Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com>

* build assets

* Update Changelog

* fix condition

* fix condition

* add comment

* change error code

* 他のエラーコードと合わせる

* Update CHANGELOG.md

---------

Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com>
---
 .config/cypress-devcontainer.yml              | 13 +++++++
 .config/example.yml                           | 13 +++++++
 CHANGELOG.md                                  |  5 +++
 cypress/e2e/basic.cy.ts                       |  1 +
 locales/index.d.ts                            | 14 +++++++
 locales/ja-JP.yml                             |  3 ++
 packages/backend/src/config.ts                |  4 ++
 .../api/endpoints/admin/accounts/create.ts    | 38 ++++++++++++++++++-
 packages/frontend/src/pages/welcome.setup.vue | 26 +++++++++++--
 packages/misskey-js/src/autogen/types.ts      |  1 +
 10 files changed, 113 insertions(+), 5 deletions(-)

diff --git a/.config/cypress-devcontainer.yml b/.config/cypress-devcontainer.yml
index 91dce35155..64988aff66 100644
--- a/.config/cypress-devcontainer.yml
+++ b/.config/cypress-devcontainer.yml
@@ -2,6 +2,19 @@
 # Misskey configuration
 #━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
 
+#   ┌────────────────────────┐
+#───┘ Initial Setup Password └─────────────────────────────────────────────────────
+
+# Password to initiate setting up admin account.
+# It will not be used after the initial setup is complete.
+#
+# Be sure to change this when you set up Misskey via the Internet.
+#
+# The provider of the service who sets up Misskey on behalf of the customer should
+# set this value to something unique when generating the Misskey config file,
+# and provide it to the customer.
+initialPassword: example_password_please_change_this_or_you_will_get_hacked
+
 #   ┌─────┐
 #───┘ URL └─────────────────────────────────────────────────────
 
diff --git a/.config/example.yml b/.config/example.yml
index 7080159117..fbc4cdff4b 100644
--- a/.config/example.yml
+++ b/.config/example.yml
@@ -59,6 +59,19 @@
 #
 # publishTarballInsteadOfProvideRepositoryUrl: true
 
+#   ┌────────────────────────┐
+#───┘ Initial Setup Password └─────────────────────────────────────────────────────
+
+# Password to initiate setting up admin account.
+# It will not be used after the initial setup is complete.
+#
+# Be sure to change this when you set up Misskey via the Internet.
+#
+# The provider of the service who sets up Misskey on behalf of the customer should
+# set this value to something unique when generating the Misskey config file,
+# and provide it to the customer.
+initialPassword: example_password_please_change_this_or_you_will_get_hacked
+
 #   ┌─────┐
 #───┘ URL └─────────────────────────────────────────────────────
 
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 188e3b7d82..2e48931267 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,11 @@
 ## 2024.10.0
 
+### Note
+- サーバー初期設定時に使用する初期パスワードを設定できるようになりました。今後Misskeyサーバーを新たに設置する際には、初回の起動前にコンフィグファイルの`initialPassword`を必ず変更してください。(すでに初期設定を完了しているサーバーについては、この変更に伴い対応する必要はありません)  
+  ホスティングサービスを運営している場合は、コンフィグファイルを構築する際に`initialPassword`をランダムな値に設定し、ユーザーに通知するようにしてください。
+
 ### General
+- Feat: サーバー初期設定時に初期パスワードを設定できるように
 - Enhance: セキュリティ向上のため、サインイン時もCAPTCHAを求めるようになりました
 - Enhance: 依存関係の更新
 - Enhance: l10nの更新
diff --git a/cypress/e2e/basic.cy.ts b/cypress/e2e/basic.cy.ts
index d2525e0a7d..e4baeacbf3 100644
--- a/cypress/e2e/basic.cy.ts
+++ b/cypress/e2e/basic.cy.ts
@@ -23,6 +23,7 @@ describe('Before setup instance', () => {
 
 		cy.intercept('POST', '/api/admin/accounts/create').as('signup');
 
+		cy.get('[data-cy-admin-initial-password] input').type('example_password_please_change_this_or_you_will_get_hacked');
 		cy.get('[data-cy-admin-username] input').type('admin');
 		cy.get('[data-cy-admin-password] input').type('admin1234');
 		cy.get('[data-cy-admin-ok]').click();
diff --git a/locales/index.d.ts b/locales/index.d.ts
index 0a9123f03d..86a6df3100 100644
--- a/locales/index.d.ts
+++ b/locales/index.d.ts
@@ -48,6 +48,20 @@ export interface Locale extends ILocale {
      * パスワード
      */
     "password": string;
+    /**
+     * 初期設定開始用パスワード
+     */
+    "initialPasswordForSetup": string;
+    /**
+     * 初期設定開始用のパスワードが違います。
+     */
+    "initialPasswordIsIncorrect": string;
+    /**
+     * Misskeyを自分でインストールした場合は、設定ファイルに入力したパスワードを使用してください。
+     * Misskeyのホスティングサービスなどを使用している場合は、提供されたパスワードを使用してください。
+     * パスワードを設定していない場合は、空欄にしたまま続行してください。
+     */
+    "initialPasswordForSetupDescription": string;
     /**
      * パスワードを忘れた
      */
diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml
index cfbe0dcc75..62317cd5e6 100644
--- a/locales/ja-JP.yml
+++ b/locales/ja-JP.yml
@@ -8,6 +8,9 @@ search: "検索"
 notifications: "通知"
 username: "ユーザー名"
 password: "パスワード"
+initialPasswordForSetup: "初期設定開始用パスワード"
+initialPasswordIsIncorrect: "初期設定開始用のパスワードが違います。"
+initialPasswordForSetupDescription: "Misskeyを自分でインストールした場合は、設定ファイルに入力したパスワードを使用してください。\nMisskeyのホスティングサービスなどを使用している場合は、提供されたパスワードを使用してください。\nパスワードを設定していない場合は、空欄にしたまま続行してください。"
 forgotPassword: "パスワードを忘れた"
 fetchingAsApObject: "連合に照会中"
 ok: "OK"
diff --git a/packages/backend/src/config.ts b/packages/backend/src/config.ts
index 97ba79c574..b320ce5403 100644
--- a/packages/backend/src/config.ts
+++ b/packages/backend/src/config.ts
@@ -63,6 +63,8 @@ type Source = {
 
 	publishTarballInsteadOfProvideRepositoryUrl?: boolean;
 
+	initialPassword?: string;
+
 	proxy?: string;
 	proxySmtp?: string;
 	proxyBypassHosts?: string[];
@@ -152,6 +154,7 @@ export type Config = {
 
 	version: string;
 	publishTarballInsteadOfProvideRepositoryUrl: boolean;
+	initialPassword: string | undefined;
 	host: string;
 	hostname: string;
 	scheme: string;
@@ -232,6 +235,7 @@ export function loadConfig(): Config {
 	return {
 		version,
 		publishTarballInsteadOfProvideRepositoryUrl: !!config.publishTarballInsteadOfProvideRepositoryUrl,
+		initialPassword: config.initialPassword,
 		url: url.origin,
 		port: config.port ?? parseInt(process.env.PORT ?? '', 10),
 		socket: config.socket,
diff --git a/packages/backend/src/server/api/endpoints/admin/accounts/create.ts b/packages/backend/src/server/api/endpoints/admin/accounts/create.ts
index a7e8a3b018..bddf7f45d3 100644
--- a/packages/backend/src/server/api/endpoints/admin/accounts/create.ts
+++ b/packages/backend/src/server/api/endpoints/admin/accounts/create.ts
@@ -12,11 +12,27 @@ import { UserEntityService } from '@/core/entities/UserEntityService.js';
 import { InstanceActorService } from '@/core/InstanceActorService.js';
 import { localUsernameSchema, passwordSchema } from '@/models/User.js';
 import { DI } from '@/di-symbols.js';
+import type { Config } from '@/config.js';
+import { ApiError } from '@/server/api/error.js';
 import { Packed } from '@/misc/json-schema.js';
 
 export const meta = {
 	tags: ['admin'],
 
+	errors: {
+		accessDenied: {
+			message: 'Access denied.',
+			code: 'ACCESS_DENIED',
+			id: '1fb7cb09-d46a-4fff-b8df-057708cce513',
+		},
+
+		wrongInitialPassword: {
+			message: 'Initial password is incorrect.',
+			code: 'INCORRECT_INITIAL_PASSWORD',
+			id: '97147c55-1ae1-4f6f-91d6-e1c3e0e76d62',
+		},
+	},
+
 	res: {
 		type: 'object',
 		optional: false, nullable: false,
@@ -35,6 +51,7 @@ export const paramDef = {
 	properties: {
 		username: localUsernameSchema,
 		password: passwordSchema,
+		initialPassword: { type: 'string', nullable: true },
 	},
 	required: ['username', 'password'],
 } as const;
@@ -42,6 +59,9 @@ export const paramDef = {
 @Injectable()
 export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
 	constructor(
+		@Inject(DI.config)
+		private config: Config,
+
 		@Inject(DI.usersRepository)
 		private usersRepository: UsersRepository,
 
@@ -52,7 +72,23 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 		super(meta, paramDef, async (ps, _me, token) => {
 			const me = _me ? await this.usersRepository.findOneByOrFail({ id: _me.id }) : null;
 			const realUsers = await this.instanceActorService.realLocalUsersPresent();
-			if ((realUsers && !me?.isRoot) || token !== null) throw new Error('access denied');
+
+			if (!realUsers && me == null && token == null) {
+				// 初回セットアップの場合
+				if (this.config.initialPassword != null) {
+					// 初期パスワードが設定されている場合
+					if (ps.initialPassword !== this.config.initialPassword) {
+						// 初期パスワードが違う場合
+						throw new ApiError(meta.errors.wrongInitialPassword);
+					}
+				} else if (ps.initialPassword != null && ps.initialPassword.trim() !== '') {
+					// 初期パスワードが設定されていないのに初期パスワードが入力された場合
+					throw new ApiError(meta.errors.wrongInitialPassword);
+				}
+			} else if ((realUsers && !me?.isRoot) || token !== null) {
+				// 初回セットアップではなく、管理者でない場合 or 外部トークンを使用している場合
+				throw new ApiError(meta.errors.accessDenied);
+			}
 
 			const { account, secret } = await this.signupService.signup({
 				username: ps.username,
diff --git a/packages/frontend/src/pages/welcome.setup.vue b/packages/frontend/src/pages/welcome.setup.vue
index a227c7c4bc..cb20cfc5fc 100644
--- a/packages/frontend/src/pages/welcome.setup.vue
+++ b/packages/frontend/src/pages/welcome.setup.vue
@@ -14,6 +14,10 @@ SPDX-License-Identifier: AGPL-3.0-only
 			</div>
 			<div class="_gaps_m" style="padding: 32px;">
 				<div>{{ i18n.ts.intro }}</div>
+				<MkInput v-model="initialPassword" type="password" data-cy-admin-initial-password>
+					<template #label>{{ i18n.ts.initialPasswordForSetup }} <div v-tooltip:dialog="i18n.ts.initialPasswordForSetupDescription" class="_button _help"><i class="ti ti-help-circle"></i></div></template>
+					<template #prefix><i class="ti ti-lock"></i></template>
+				</MkInput>
 				<MkInput v-model="username" pattern="^[a-zA-Z0-9_]{1,20}$" :spellcheck="false" required data-cy-admin-username>
 					<template #label>{{ i18n.ts.username }}</template>
 					<template #prefix>@</template>
@@ -47,6 +51,7 @@ import MkAnimBg from '@/components/MkAnimBg.vue';
 
 const username = ref('');
 const password = ref('');
+const initialPassword = ref('');
 const submitting = ref(false);
 
 function submit() {
@@ -56,14 +61,27 @@ function submit() {
 	misskeyApi('admin/accounts/create', {
 		username: username.value,
 		password: password.value,
+		initialPassword: initialPassword.value === '' ? null : initialPassword.value,
 	}).then(res => {
 		return login(res.token);
-	}).catch(() => {
+	}).catch((err) => {
 		submitting.value = false;
 
+		let title = i18n.ts.somethingHappened;
+		let text = err.message + '\n' + err.id;
+
+		if (err.code === 'ACCESS_DENIED') {
+			title = i18n.ts.permissionDeniedError;
+			text = i18n.ts.operationForbidden;
+		} else if (err.code === 'INCORRECT_INITIAL_PASSWORD') {
+			title = i18n.ts.permissionDeniedError;
+			text = i18n.ts.incorrectPassword;
+		}
+
 		os.alert({
 			type: 'error',
-			text: i18n.ts.somethingHappened,
+			title,
+			text,
 		});
 	});
 }
@@ -74,8 +92,8 @@ function submit() {
 	min-height: 100svh;
 	padding: 32px 32px 64px 32px;
 	box-sizing: border-box;
-display: grid;
-place-content: center;
+	display: grid;
+	place-content: center;
 }
 
 .form {
diff --git a/packages/misskey-js/src/autogen/types.ts b/packages/misskey-js/src/autogen/types.ts
index 46fc2496da..ee5cd477f1 100644
--- a/packages/misskey-js/src/autogen/types.ts
+++ b/packages/misskey-js/src/autogen/types.ts
@@ -5611,6 +5611,7 @@ export type operations = {
         'application/json': {
           username: string;
           password: string;
+          initialPassword?: string | null;
         };
       };
     };

From 2a4ab0e1878c7e45e939574cb4eb6c23f6371802 Mon Sep 17 00:00:00 2001
From: zyoshoka <107108195+zyoshoka@users.noreply.github.com>
Date: Thu, 3 Oct 2024 18:33:56 +0900
Subject: [PATCH 019/121] fix(misskey-js): type fixes related to signup and
 signin (#14679)

---
 .../src/server/api/ApiServerService.ts        | 12 +++---
 packages/frontend/src/components/MkSignin.vue |  5 +--
 packages/misskey-js/etc/misskey-js.api.md     | 37 ++++++++++++++++---
 packages/misskey-js/package.json              |  1 +
 packages/misskey-js/src/api.types.ts          | 17 ++++++++-
 packages/misskey-js/src/entities.ts           | 18 +++++++--
 pnpm-lock.yaml                                |  3 ++
 7 files changed, 73 insertions(+), 20 deletions(-)

diff --git a/packages/backend/src/server/api/ApiServerService.ts b/packages/backend/src/server/api/ApiServerService.ts
index 709a044601..356e145681 100644
--- a/packages/backend/src/server/api/ApiServerService.ts
+++ b/packages/backend/src/server/api/ApiServerService.ts
@@ -118,6 +118,7 @@ export class ApiServerService {
 				'hcaptcha-response'?: string;
 				'g-recaptcha-response'?: string;
 				'turnstile-response'?: string;
+				'm-captcha-response'?: string;
 			}
 		}>('/signup', (request, reply) => this.signupApiService.signup(request, reply));
 
@@ -126,17 +127,18 @@ export class ApiServerService {
 				username: string;
 				password: string;
 				token?: string;
-				signature?: string;
-				authenticatorData?: string;
-				clientDataJSON?: string;
-				credentialId?: string;
-				challengeId?: string;
+				credential?: AuthenticationResponseJSON;
+				'hcaptcha-response'?: string;
+				'g-recaptcha-response'?: string;
+				'turnstile-response'?: string;
+				'm-captcha-response'?: string;
 			};
 		}>('/signin', (request, reply) => this.signinApiService.signin(request, reply));
 
 		fastify.post<{
 			Body: {
 				credential?: AuthenticationResponseJSON;
+				context?: string;
 			};
 		}>('/signin-with-passkey', (request, reply) => this.signinWithPasskeyApiService.signin(request, reply));
 
diff --git a/packages/frontend/src/components/MkSignin.vue b/packages/frontend/src/components/MkSignin.vue
index 8ebdac0220..abbff8e1f2 100644
--- a/packages/frontend/src/components/MkSignin.vue
+++ b/packages/frontend/src/components/MkSignin.vue
@@ -76,7 +76,6 @@ import { computed, defineAsyncComponent, ref } from 'vue';
 import { toUnicode } from 'punycode/';
 import * as Misskey from 'misskey-js';
 import { supported as webAuthnSupported, get as webAuthnRequest, parseRequestOptionsFromJSON } from '@github/webauthn-json/browser-ponyfill';
-import { SigninWithPasskeyResponse } from 'misskey-js/entities.js';
 import { query, extractDomain } from '@@/js/url.js';
 import { host as configHost } from '@@/js/config.js';
 import MkDivider from './MkDivider.vue';
@@ -188,7 +187,7 @@ function onPasskeyLogin(): void {
 	signing.value = true;
 	if (webAuthnSupported()) {
 		misskeyApi('signin-with-passkey', {})
-			.then((res: SigninWithPasskeyResponse) => {
+			.then(res => {
 				totpLogin.value = false;
 				signing.value = false;
 				queryingKey.value = true;
@@ -219,7 +218,7 @@ async function queryPasskey(): Promise<void> {
 				credential: credential.toJSON(),
 				context: passkey_context.value,
 			});
-		}).then((res: SigninWithPasskeyResponse) => {
+		}).then(res => {
 			emit('login', res.signinResponse);
 			return onLogin(res.signinResponse);
 		});
diff --git a/packages/misskey-js/etc/misskey-js.api.md b/packages/misskey-js/etc/misskey-js.api.md
index a5f12b41f4..5f4792eb74 100644
--- a/packages/misskey-js/etc/misskey-js.api.md
+++ b/packages/misskey-js/etc/misskey-js.api.md
@@ -4,7 +4,9 @@
 
 ```ts
 
+import type { AuthenticationResponseJSON } from '@simplewebauthn/types';
 import { EventEmitter } from 'eventemitter3';
+import type { PublicKeyCredentialRequestOptionsJSON } from '@simplewebauthn/types';
 
 // Warning: (ae-forgotten-export) The symbol "components" needs to be exported by the entry point index.d.ts
 //
@@ -1162,7 +1164,19 @@ export type Endpoints = Overwrite<Endpoints_2, {
     };
     'signin-with-passkey': {
         req: SigninWithPasskeyRequest;
-        res: SigninWithPasskeyResponse;
+        res: {
+            $switch: {
+                $cases: [
+                [
+                    {
+                    context: string;
+                },
+                SigninWithPasskeyResponse
+                ]
+                ];
+                $default: SigninWithPasskeyInitResponse;
+            };
+        };
     };
     'admin/roles/create': {
         req: Overwrite<AdminRolesCreateRequest, {
@@ -1196,6 +1210,7 @@ declare namespace entities {
         SignupPendingResponse,
         SigninRequest,
         SigninWithPasskeyRequest,
+        SigninWithPasskeyInitResponse,
         SigninWithPasskeyResponse,
         SigninResponse,
         PartialRolePolicyOverride,
@@ -3027,6 +3042,11 @@ type SigninRequest = {
     username: string;
     password: string;
     token?: string;
+    credential?: AuthenticationResponseJSON;
+    'hcaptcha-response'?: string | null;
+    'g-recaptcha-response'?: string | null;
+    'turnstile-response'?: string | null;
+    'm-captcha-response'?: string | null;
 };
 
 // @public (undocumented)
@@ -3035,17 +3055,21 @@ type SigninResponse = {
     i: string;
 };
 
+// @public (undocumented)
+type SigninWithPasskeyInitResponse = {
+    option: PublicKeyCredentialRequestOptionsJSON;
+    context: string;
+};
+
 // @public (undocumented)
 type SigninWithPasskeyRequest = {
-    credential?: object;
+    credential?: AuthenticationResponseJSON;
     context?: string;
 };
 
 // @public (undocumented)
 type SigninWithPasskeyResponse = {
-    option?: object;
-    context?: string;
-    signinResponse?: SigninResponse;
+    signinResponse: SigninResponse;
 };
 
 // @public (undocumented)
@@ -3069,6 +3093,7 @@ type SignupRequest = {
     'hcaptcha-response'?: string | null;
     'g-recaptcha-response'?: string | null;
     'turnstile-response'?: string | null;
+    'm-captcha-response'?: string | null;
 };
 
 // @public (undocumented)
@@ -3346,7 +3371,7 @@ type UsersUpdateMemoRequest = operations['users___update-memo']['requestBody']['
 
 // Warnings were encountered during analysis:
 //
-// src/entities.ts:49:2 - (ae-forgotten-export) The symbol "ModerationLogPayloads" needs to be exported by the entry point index.d.ts
+// src/entities.ts:50:2 - (ae-forgotten-export) The symbol "ModerationLogPayloads" needs to be exported by the entry point index.d.ts
 // src/streaming.types.ts:220:4 - (ae-forgotten-export) The symbol "ReversiUpdateKey" needs to be exported by the entry point index.d.ts
 // src/streaming.types.ts:230:4 - (ae-forgotten-export) The symbol "ReversiUpdateSettings" needs to be exported by the entry point index.d.ts
 
diff --git a/packages/misskey-js/package.json b/packages/misskey-js/package.json
index b41f0057a3..badc4f64ff 100644
--- a/packages/misskey-js/package.json
+++ b/packages/misskey-js/package.json
@@ -57,6 +57,7 @@
 		"built"
 	],
 	"dependencies": {
+		"@simplewebauthn/types": "10.0.0",
 		"eventemitter3": "5.0.1",
 		"reconnecting-websocket": "4.4.0"
 	}
diff --git a/packages/misskey-js/src/api.types.ts b/packages/misskey-js/src/api.types.ts
index 4c3f2e1578..cef5ab8861 100644
--- a/packages/misskey-js/src/api.types.ts
+++ b/packages/misskey-js/src/api.types.ts
@@ -5,6 +5,7 @@ import {
 	PartialRolePolicyOverride,
 	SigninRequest,
 	SigninResponse,
+	SigninWithPasskeyInitResponse,
 	SigninWithPasskeyRequest,
 	SigninWithPasskeyResponse,
 	SignupPendingRequest,
@@ -86,8 +87,20 @@ export type Endpoints = Overwrite<
 		},
 		'signin-with-passkey': {
 			req: SigninWithPasskeyRequest;
-			res: SigninWithPasskeyResponse;
-		}
+			res: {
+				$switch: {
+					$cases: [
+						[
+							{
+								context: string;
+							},
+							SigninWithPasskeyResponse,
+						],
+					];
+					$default: SigninWithPasskeyInitResponse;
+				},
+			},
+		},
 		'admin/roles/create': {
 			req: Overwrite<AdminRolesCreateRequest, { policies: PartialRolePolicyOverride }>;
 			res: AdminRolesCreateResponse;
diff --git a/packages/misskey-js/src/entities.ts b/packages/misskey-js/src/entities.ts
index 64ed90cbb1..36b7f5bca3 100644
--- a/packages/misskey-js/src/entities.ts
+++ b/packages/misskey-js/src/entities.ts
@@ -10,6 +10,7 @@ import {
 	User,
 	UserDetailedNotMe,
 } from './autogen/models.js';
+import type { AuthenticationResponseJSON, PublicKeyCredentialRequestOptionsJSON } from '@simplewebauthn/types';
 
 export * from './autogen/entities.js';
 export * from './autogen/models.js';
@@ -250,6 +251,7 @@ export type SignupRequest = {
 	'hcaptcha-response'?: string | null;
 	'g-recaptcha-response'?: string | null;
 	'turnstile-response'?: string | null;
+	'm-captcha-response'?: string | null;
 }
 
 export type SignupResponse = MeDetailed & {
@@ -269,17 +271,25 @@ export type SigninRequest = {
 	username: string;
 	password: string;
 	token?: string;
+	credential?: AuthenticationResponseJSON;
+	'hcaptcha-response'?: string | null;
+	'g-recaptcha-response'?: string | null;
+	'turnstile-response'?: string | null;
+	'm-captcha-response'?: string | null;
 };
 
 export type SigninWithPasskeyRequest = {
-	credential?: object;
+	credential?: AuthenticationResponseJSON;
 	context?: string;
 };
 
+export type SigninWithPasskeyInitResponse = {
+	option: PublicKeyCredentialRequestOptionsJSON;
+	context: string;
+};
+
 export type SigninWithPasskeyResponse = {
-	option?: object;
-	context?: string;
-	signinResponse?: SigninResponse;
+	signinResponse: SigninResponse;
 };
 
 export type SigninResponse = {
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 5d7febbcc9..53d5dbbde0 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -1289,6 +1289,9 @@ importers:
 
   packages/misskey-js:
     dependencies:
+      '@simplewebauthn/types':
+        specifier: 10.0.0
+        version: 10.0.0
       eventemitter3:
         specifier: 5.0.1
         version: 5.0.1

From d2175a9b9f6e38ca3ec0ca28b29d99f4b46f9dcd Mon Sep 17 00:00:00 2001
From: syuilo <4439005+syuilo@users.noreply.github.com>
Date: Thu, 3 Oct 2024 20:40:39 +0900
Subject: [PATCH 020/121] initialPassword -> setupPassword

---
 .config/cypress-devcontainer.yml                          | 2 +-
 .config/example.yml                                       | 2 +-
 packages/backend/src/config.ts                            | 6 +++---
 .../src/server/api/endpoints/admin/accounts/create.ts     | 8 ++++----
 packages/frontend/src/pages/welcome.setup.vue             | 8 ++++----
 packages/misskey-js/src/autogen/types.ts                  | 2 +-
 6 files changed, 14 insertions(+), 14 deletions(-)

diff --git a/.config/cypress-devcontainer.yml b/.config/cypress-devcontainer.yml
index 64988aff66..3907615f73 100644
--- a/.config/cypress-devcontainer.yml
+++ b/.config/cypress-devcontainer.yml
@@ -13,7 +13,7 @@
 # The provider of the service who sets up Misskey on behalf of the customer should
 # set this value to something unique when generating the Misskey config file,
 # and provide it to the customer.
-initialPassword: example_password_please_change_this_or_you_will_get_hacked
+setupPassword: example_password_please_change_this_or_you_will_get_hacked
 
 #   ┌─────┐
 #───┘ URL └─────────────────────────────────────────────────────
diff --git a/.config/example.yml b/.config/example.yml
index fbc4cdff4b..600c1c632e 100644
--- a/.config/example.yml
+++ b/.config/example.yml
@@ -70,7 +70,7 @@
 # The provider of the service who sets up Misskey on behalf of the customer should
 # set this value to something unique when generating the Misskey config file,
 # and provide it to the customer.
-initialPassword: example_password_please_change_this_or_you_will_get_hacked
+setupPassword: example_password_please_change_this_or_you_will_get_hacked
 
 #   ┌─────┐
 #───┘ URL └─────────────────────────────────────────────────────
diff --git a/packages/backend/src/config.ts b/packages/backend/src/config.ts
index b320ce5403..42f1033b9d 100644
--- a/packages/backend/src/config.ts
+++ b/packages/backend/src/config.ts
@@ -63,7 +63,7 @@ type Source = {
 
 	publishTarballInsteadOfProvideRepositoryUrl?: boolean;
 
-	initialPassword?: string;
+	setupPassword?: string;
 
 	proxy?: string;
 	proxySmtp?: string;
@@ -154,7 +154,7 @@ export type Config = {
 
 	version: string;
 	publishTarballInsteadOfProvideRepositoryUrl: boolean;
-	initialPassword: string | undefined;
+	setupPassword: string | undefined;
 	host: string;
 	hostname: string;
 	scheme: string;
@@ -235,7 +235,7 @@ export function loadConfig(): Config {
 	return {
 		version,
 		publishTarballInsteadOfProvideRepositoryUrl: !!config.publishTarballInsteadOfProvideRepositoryUrl,
-		initialPassword: config.initialPassword,
+		setupPassword: config.setupPassword,
 		url: url.origin,
 		port: config.port ?? parseInt(process.env.PORT ?? '', 10),
 		socket: config.socket,
diff --git a/packages/backend/src/server/api/endpoints/admin/accounts/create.ts b/packages/backend/src/server/api/endpoints/admin/accounts/create.ts
index bddf7f45d3..d30131a62f 100644
--- a/packages/backend/src/server/api/endpoints/admin/accounts/create.ts
+++ b/packages/backend/src/server/api/endpoints/admin/accounts/create.ts
@@ -51,7 +51,7 @@ export const paramDef = {
 	properties: {
 		username: localUsernameSchema,
 		password: passwordSchema,
-		initialPassword: { type: 'string', nullable: true },
+		setupPassword: { type: 'string', nullable: true },
 	},
 	required: ['username', 'password'],
 } as const;
@@ -75,13 +75,13 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 
 			if (!realUsers && me == null && token == null) {
 				// 初回セットアップの場合
-				if (this.config.initialPassword != null) {
+				if (this.config.setupPassword != null) {
 					// 初期パスワードが設定されている場合
-					if (ps.initialPassword !== this.config.initialPassword) {
+					if (ps.setupPassword !== this.config.setupPassword) {
 						// 初期パスワードが違う場合
 						throw new ApiError(meta.errors.wrongInitialPassword);
 					}
-				} else if (ps.initialPassword != null && ps.initialPassword.trim() !== '') {
+				} else if (ps.setupPassword != null && ps.setupPassword.trim() !== '') {
 					// 初期パスワードが設定されていないのに初期パスワードが入力された場合
 					throw new ApiError(meta.errors.wrongInitialPassword);
 				}
diff --git a/packages/frontend/src/pages/welcome.setup.vue b/packages/frontend/src/pages/welcome.setup.vue
index cb20cfc5fc..dd258aad98 100644
--- a/packages/frontend/src/pages/welcome.setup.vue
+++ b/packages/frontend/src/pages/welcome.setup.vue
@@ -14,7 +14,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 			</div>
 			<div class="_gaps_m" style="padding: 32px;">
 				<div>{{ i18n.ts.intro }}</div>
-				<MkInput v-model="initialPassword" type="password" data-cy-admin-initial-password>
+				<MkInput v-model="setupPassword" type="password" data-cy-admin-initial-password>
 					<template #label>{{ i18n.ts.initialPasswordForSetup }} <div v-tooltip:dialog="i18n.ts.initialPasswordForSetupDescription" class="_button _help"><i class="ti ti-help-circle"></i></div></template>
 					<template #prefix><i class="ti ti-lock"></i></template>
 				</MkInput>
@@ -40,9 +40,9 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <script lang="ts" setup>
 import { ref } from 'vue';
+import { host, version } from '@@/js/config.js';
 import MkButton from '@/components/MkButton.vue';
 import MkInput from '@/components/MkInput.vue';
-import { host, version } from '@@/js/config.js';
 import * as os from '@/os.js';
 import { misskeyApi } from '@/scripts/misskey-api.js';
 import { login } from '@/account.js';
@@ -51,7 +51,7 @@ import MkAnimBg from '@/components/MkAnimBg.vue';
 
 const username = ref('');
 const password = ref('');
-const initialPassword = ref('');
+const setupPassword = ref('');
 const submitting = ref(false);
 
 function submit() {
@@ -61,7 +61,7 @@ function submit() {
 	misskeyApi('admin/accounts/create', {
 		username: username.value,
 		password: password.value,
-		initialPassword: initialPassword.value === '' ? null : initialPassword.value,
+		setupPassword: setupPassword.value === '' ? null : setupPassword.value,
 	}).then(res => {
 		return login(res.token);
 	}).catch((err) => {
diff --git a/packages/misskey-js/src/autogen/types.ts b/packages/misskey-js/src/autogen/types.ts
index ee5cd477f1..32646d28ed 100644
--- a/packages/misskey-js/src/autogen/types.ts
+++ b/packages/misskey-js/src/autogen/types.ts
@@ -5611,7 +5611,7 @@ export type operations = {
         'application/json': {
           username: string;
           password: string;
-          initialPassword?: string | null;
+          setupPassword?: string | null;
         };
       };
     };

From d266c3cdf470f5b702f0784eacd4f35a1f2308c5 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=8B=E3=81=A3=E3=81=93=E3=81=8B=E3=82=8A?=
 <67428053+kakkokari-gtyih@users.noreply.github.com>
Date: Thu, 3 Oct 2024 20:52:31 +0900
Subject: [PATCH 021/121] =?UTF-8?q?fix(gh):=20Github=E3=81=AE=E3=83=86?=
 =?UTF-8?q?=E3=82=B9=E3=83=88=E7=94=A8=E7=92=B0=E5=A2=83=E3=81=A7setupPass?=
 =?UTF-8?q?word=E3=81=8C=E6=8C=87=E5=AE=9A=E3=81=95=E3=82=8C=E3=81=A6?=
 =?UTF-8?q?=E3=81=84=E3=81=AA=E3=81=84=E3=81=AE=E3=82=92=E4=BF=AE=E6=AD=A3?=
 =?UTF-8?q?=20(#14681)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .github/misskey/test.yml | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/.github/misskey/test.yml b/.github/misskey/test.yml
index 7a4aa4ae6c..3c807e8b9e 100644
--- a/.github/misskey/test.yml
+++ b/.github/misskey/test.yml
@@ -1,5 +1,7 @@
 url: 'http://misskey.local'
 
+setupPassword: example_password_please_change_this_or_you_will_get_hacked
+
 # ローカルでテストするときにポートを被らないようにするためデフォルトのものとは変える(以下同じ)
 port: 61812
 

From 7bdc4e8509de914b0d11e94d5b6da88f54430a51 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=8B=E3=81=A3=E3=81=93=E3=81=8B=E3=82=8A?=
 <67428053+kakkokari-gtyih@users.noreply.github.com>
Date: Thu, 3 Oct 2024 21:01:09 +0900
Subject: [PATCH 022/121] =?UTF-8?q?fix:=20=E5=88=9D=E6=9C=9F=E3=83=91?=
 =?UTF-8?q?=E3=82=B9=E3=83=AF=E3=83=BC=E3=83=89=E3=82=92=E3=82=B3=E3=83=A1?=
 =?UTF-8?q?=E3=83=B3=E3=83=88=E3=82=A2=E3=82=A6=E3=83=88=20(#14682)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

* fix: 初期パスワードをコメントアウト

* :art:

* fix indent
---
 .config/example.yml | 3 ++-
 CHANGELOG.md        | 5 +++--
 2 files changed, 5 insertions(+), 3 deletions(-)

diff --git a/.config/example.yml b/.config/example.yml
index 600c1c632e..60a6a0aa71 100644
--- a/.config/example.yml
+++ b/.config/example.yml
@@ -70,7 +70,8 @@
 # The provider of the service who sets up Misskey on behalf of the customer should
 # set this value to something unique when generating the Misskey config file,
 # and provide it to the customer.
-setupPassword: example_password_please_change_this_or_you_will_get_hacked
+#
+# setupPassword: example_password_please_change_this_or_you_will_get_hacked
 
 #   ┌─────┐
 #───┘ URL └─────────────────────────────────────────────────────
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2e48931267..9e4ddabd55 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,8 +1,9 @@
 ## 2024.10.0
 
 ### Note
-- サーバー初期設定時に使用する初期パスワードを設定できるようになりました。今後Misskeyサーバーを新たに設置する際には、初回の起動前にコンフィグファイルの`initialPassword`を必ず変更してください。(すでに初期設定を完了しているサーバーについては、この変更に伴い対応する必要はありません)  
-  ホスティングサービスを運営している場合は、コンフィグファイルを構築する際に`initialPassword`をランダムな値に設定し、ユーザーに通知するようにしてください。
+- サーバー初期設定時に使用する初期パスワードを設定できるようになりました。今後Misskeyサーバーを新たに設置する際には、初回の起動前にコンフィグファイルの`setupPassword`をコメントアウトし、初期パスワードを設定することをおすすめします。(すでに初期設定を完了しているサーバーについては、この変更に伴い対応する必要はありません)  
+  - ホスティングサービスを運営している場合は、コンフィグファイルを構築する際に`setupPassword`をランダムな値に設定し、ユーザーに通知するようにシステムを更新することをおすすめします。
+  - なお、初期パスワードが設定されていない場合でも初期設定を行うことが可能です(UI上で初期パスワードの入力欄を空欄にすると続行できます)。
 
 ### General
 - Feat: サーバー初期設定時に初期パスワードを設定できるように

From fa2558fce898b2c13a046e384f6cff24413dab04 Mon Sep 17 00:00:00 2001
From: "github-actions[bot]" <github-actions[bot]@users.noreply.github.com>
Date: Thu, 3 Oct 2024 12:02:35 +0000
Subject: [PATCH 023/121] Bump version to 2024.10.0-alpha.1

---
 package.json                     | 2 +-
 packages/misskey-js/package.json | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/package.json b/package.json
index a463bdb7b8..fd8f828773 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
 	"name": "misskey",
-	"version": "2024.10.0-alpha.0",
+	"version": "2024.10.0-alpha.1",
 	"codename": "nasubi",
 	"repository": {
 		"type": "git",
diff --git a/packages/misskey-js/package.json b/packages/misskey-js/package.json
index badc4f64ff..b3e7a6a20a 100644
--- a/packages/misskey-js/package.json
+++ b/packages/misskey-js/package.json
@@ -1,7 +1,7 @@
 {
 	"type": "module",
 	"name": "misskey-js",
-	"version": "2024.10.0-alpha.0",
+	"version": "2024.10.0-alpha.1",
 	"description": "Misskey SDK for JavaScript",
 	"license": "MIT",
 	"main": "./built/index.js",

From 650e22c90d22af1e6ffdbad2f17cb4a59c1ee7d4 Mon Sep 17 00:00:00 2001
From: "github-actions[bot]" <github-actions[bot]@users.noreply.github.com>
Date: Thu, 3 Oct 2024 12:47:03 +0000
Subject: [PATCH 024/121] Bump version to 2024.10.0-beta.2

---
 package.json                     | 2 +-
 packages/misskey-js/package.json | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/package.json b/package.json
index fd8f828773..626b679a95 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
 	"name": "misskey",
-	"version": "2024.10.0-alpha.1",
+	"version": "2024.10.0-beta.2",
 	"codename": "nasubi",
 	"repository": {
 		"type": "git",
diff --git a/packages/misskey-js/package.json b/packages/misskey-js/package.json
index b3e7a6a20a..a5f96647ed 100644
--- a/packages/misskey-js/package.json
+++ b/packages/misskey-js/package.json
@@ -1,7 +1,7 @@
 {
 	"type": "module",
 	"name": "misskey-js",
-	"version": "2024.10.0-alpha.1",
+	"version": "2024.10.0-beta.2",
 	"description": "Misskey SDK for JavaScript",
 	"license": "MIT",
 	"main": "./built/index.js",

From a08a38c29ac7a2f78872a76a50a76e0a4bd5c9b1 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=8B=E3=81=A3=E3=81=93=E3=81=8B=E3=82=8A?=
 <67428053+kakkokari-gtyih@users.noreply.github.com>
Date: Fri, 4 Oct 2024 07:54:19 +0900
Subject: [PATCH 025/121] =?UTF-8?q?fix(test):=20=E5=88=9D=E6=9C=9F?=
 =?UTF-8?q?=E3=82=BB=E3=83=83=E3=83=88=E3=82=A2=E3=83=83=E3=83=97=E3=81=A7?=
 =?UTF-8?q?=E5=88=9D=E6=9C=9F=E3=83=91=E3=82=B9=E3=83=AF=E3=83=BC=E3=83=89?=
 =?UTF-8?q?=E3=82=92=E5=85=A5=E5=8A=9B=E3=81=97=E3=81=A6=E3=81=84=E3=81=AA?=
 =?UTF-8?q?=E3=81=84=E3=81=AE=E3=82=92=E4=BF=AE=E6=AD=A3=20(#14685)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 cypress/support/commands.ts | 1 +
 1 file changed, 1 insertion(+)

diff --git a/cypress/support/commands.ts b/cypress/support/commands.ts
index 281f2e6ccd..3cdf4e2087 100644
--- a/cypress/support/commands.ts
+++ b/cypress/support/commands.ts
@@ -48,6 +48,7 @@ Cypress.Commands.add('registerUser', (username, password, isAdmin = false) => {
 	cy.request('POST', route, {
 		username: username,
 		password: password,
+		...(isAdmin ? { setupPassword: 'example_password_please_change_this_or_you_will_get_hacked' } : {}),
 	}).its('body').as(username);
 });
 

From c1597be45806be25974a11bacc09dcc77c0ae96c Mon Sep 17 00:00:00 2001
From: syuilo <4439005+syuilo@users.noreply.github.com>
Date: Fri, 4 Oct 2024 10:18:36 +0900
Subject: [PATCH 026/121] :art:

---
 .../frontend/src/components/MkAbuseReport.vue | 142 +++++++++---------
 packages/frontend/src/components/MkFolder.vue |   7 +-
 .../src/components/global/RouterView.vue      |   3 +
 packages/frontend/src/pages/admin/abuses.vue  |   6 +-
 packages/frontend/src/pages/admin/index.vue   |   2 +-
 .../frontend/src/pages/settings/index.vue     |   2 +-
 6 files changed, 83 insertions(+), 79 deletions(-)

diff --git a/packages/frontend/src/components/MkAbuseReport.vue b/packages/frontend/src/components/MkAbuseReport.vue
index a28e7c2559..aa2bffaa17 100644
--- a/packages/frontend/src/components/MkAbuseReport.vue
+++ b/packages/frontend/src/components/MkAbuseReport.vue
@@ -4,64 +4,98 @@ SPDX-License-Identifier: AGPL-3.0-only
 -->
 
 <template>
-<div class="bcekxzvu _margin _panel">
-	<div class="target">
-		<MkA v-user-preview="report.targetUserId" class="info" :to="`/admin/user/${report.targetUserId}`" :behavior="'window'">
-			<MkAvatar class="avatar" :user="report.targetUser" indicator/>
-			<div class="names">
-				<MkUserName class="name" :user="report.targetUser"/>
-				<MkAcct class="acct" :user="report.targetUser" style="display: block;"/>
-			</div>
-		</MkA>
-		<MkKeyValue>
-			<template #key>{{ i18n.ts.registeredDate }}</template>
-			<template #value>{{ dateString(report.targetUser.createdAt) }} (<MkTime :time="report.targetUser.createdAt"/>)</template>
-		</MkKeyValue>
-	</div>
-	<div class="detail">
-		<div>
-			<Mfm :text="report.comment" :linkNavigationBehavior="'window'"/>
+<MkFolder>
+	<template #icon>
+		<i v-if="report.resolved" class="ti ti-check" style="color: var(--success)"></i>
+		<i v-else class="ti ti-exclamation-circle" style="color: var(--warn)"></i>
+	</template>
+	<template #label><MkAcct :user="report.targetUser"/> (by <MkAcct :user="report.reporter"/>)</template>
+	<template #suffix><MkTime :time="report.createdAt"/></template>
+	<template v-if="!report.resolved" #footer>
+		<div class="_buttons">
+			<MkButton primary @click="resolve">{{ i18n.ts.abuseMarkAsResolved }}</MkButton>
+			<template v-if="report.targetUser.host == null || report.resolved">
+				<MkButton primary @click="resolveAndForward">{{ i18n.ts.forwardReport }}</MkButton>
+				<div v-tooltip:dialog="i18n.ts.forwardReportIsAnonymous" class="_button _help"><i class="ti ti-help-circle"></i></div>
+			</template>
 		</div>
-		<hr/>
-		<div>{{ i18n.ts.reporter }}: <MkA :to="`/admin/user/${report.reporter.id}`" class="_link" :behavior="'window'">@{{ report.reporter.username }}</MkA></div>
+	</template>
+
+	<div :class="$style.root" class="_gaps_s">
+		<MkFolder :withSpacer="false">
+			<template #icon><MkAvatar :user="report.targetUser" style="width: 18px; height: 18px;"/></template>
+			<template #label>Target: <MkAcct :user="report.targetUser"/></template>
+			<template #suffix>#{{ report.targetUserId.toUpperCase() }}</template>
+
+			<div style="container-type: inline-size;">
+				<RouterView :router="targetRouter"/>
+			</div>
+		</MkFolder>
+
+		<MkFolder :defaultOpen="true">
+			<template #icon><i class="ti ti-message-2"></i></template>
+			<template #label>{{ i18n.ts.details }}</template>
+			<div>
+				<Mfm :text="report.comment" :linkNavigationBehavior="'window'"/>
+			</div>
+		</MkFolder>
+
+		<MkFolder :withSpacer="false">
+			<template #icon><MkAvatar :user="report.reporter" style="width: 18px; height: 18px;"/></template>
+			<template #label>{{ i18n.ts.reporter }}: <MkAcct :user="report.reporter"/></template>
+			<template #suffix>#{{ report.reporterId.toUpperCase() }}</template>
+
+			<div style="container-type: inline-size;">
+				<RouterView :router="reporterRouter"/>
+			</div>
+		</MkFolder>
+
 		<div v-if="report.assignee">
 			{{ i18n.ts.moderator }}:
 			<MkAcct :user="report.assignee"/>
 		</div>
-		<div><MkTime :time="report.createdAt"/></div>
-		<div class="action">
-			<MkSwitch v-model="forward" :disabled="report.targetUser.host == null || report.resolved">
-				{{ i18n.ts.forwardReport }}
-				<template #caption>{{ i18n.ts.forwardReportIsAnonymous }}</template>
-			</MkSwitch>
-			<MkButton v-if="!report.resolved" primary @click="resolve">{{ i18n.ts.abuseMarkAsResolved }}</MkButton>
-		</div>
 	</div>
-</div>
+</MkFolder>
 </template>
 
 <script lang="ts" setup>
-import { ref } from 'vue';
+import { provide, ref } from 'vue';
+import * as Misskey from 'misskey-js';
 import MkButton from '@/components/MkButton.vue';
 import MkSwitch from '@/components/MkSwitch.vue';
 import MkKeyValue from '@/components/MkKeyValue.vue';
 import * as os from '@/os.js';
 import { i18n } from '@/i18n.js';
 import { dateString } from '@/filters/date.js';
+import MkFolder from '@/components/MkFolder.vue';
+import RouterView from '@/components/global/RouterView.vue';
+import { useRouterFactory } from '@/router/supplier';
 
 const props = defineProps<{
-	report: any;
+	report: Misskey.entities.AdminAbuseUserReportsResponse[number];
 }>();
 
 const emit = defineEmits<{
 	(ev: 'resolved', reportId: string): void;
 }>();
 
-const forward = ref(props.report.forwarded);
+const routerFactory = useRouterFactory();
+const targetRouter = routerFactory(`/admin/user/${props.report.targetUserId}`);
+targetRouter.init();
+const reporterRouter = routerFactory(`/admin/user/${props.report.reporterId}`);
+reporterRouter.init();
 
 function resolve() {
 	os.apiWithDialog('admin/resolve-abuse-user-report', {
-		forward: forward.value,
+		reportId: props.report.id,
+	}).then(() => {
+		emit('resolved', props.report.id);
+	});
+}
+
+function resolveAndForward() {
+	os.apiWithDialog('admin/resolve-abuse-user-report', {
+		forward: true,
 		reportId: props.report.id,
 	}).then(() => {
 		emit('resolved', props.report.id);
@@ -69,47 +103,7 @@ function resolve() {
 }
 </script>
 
-<style lang="scss" scoped>
-.bcekxzvu {
-	display: flex;
-
-	> .target {
-		width: 35%;
-		box-sizing: border-box;
-		text-align: left;
-		padding: 24px;
-		border-right: solid 1px var(--divider);
-
-		> .info {
-			display: flex;
-			box-sizing: border-box;
-			align-items: center;
-			padding: 14px;
-			border-radius: 8px;
-			--c: rgb(255 196 0 / 15%);
-			background-image: linear-gradient(45deg, var(--c) 16.67%, transparent 16.67%, transparent 50%, var(--c) 50%, var(--c) 66.67%, transparent 66.67%, transparent 100%);
-			background-size: 16px 16px;
-
-			> .avatar {
-				width: 42px;
-				height: 42px;
-			}
-
-			> .names {
-				margin-left: 0.3em;
-				padding: 0 8px;
-				flex: 1;
-
-				> .name {
-					font-weight: bold;
-				}
-			}
-		}
-	}
-
-	> .detail {
-		flex: 1;
-		padding: 24px;
-	}
+<style lang="scss" module>
+.root {
 }
 </style>
diff --git a/packages/frontend/src/components/MkFolder.vue b/packages/frontend/src/components/MkFolder.vue
index a5f3069d45..8262ae5d0c 100644
--- a/packages/frontend/src/components/MkFolder.vue
+++ b/packages/frontend/src/components/MkFolder.vue
@@ -38,9 +38,12 @@ SPDX-License-Identifier: AGPL-3.0-only
 			>
 				<KeepAlive>
 					<div v-show="opened">
-						<MkSpacer :marginMin="14" :marginMax="22">
+						<MkSpacer v-if="withSpacer" :marginMin="14" :marginMax="22">
 							<slot></slot>
 						</MkSpacer>
+						<div v-else>
+							<slot></slot>
+						</div>
 						<div v-if="$slots.footer" :class="$style.footer">
 							<slot name="footer"></slot>
 						</div>
@@ -59,9 +62,11 @@ import { defaultStore } from '@/store.js';
 const props = withDefaults(defineProps<{
 	defaultOpen?: boolean;
 	maxHeight?: number | null;
+	withSpacer?: boolean;
 }>(), {
 	defaultOpen: false,
 	maxHeight: null,
+	withSpacer: true,
 });
 
 const getBgColor = (el: HTMLElement) => {
diff --git a/packages/frontend/src/components/global/RouterView.vue b/packages/frontend/src/components/global/RouterView.vue
index 19bd794a5d..38bdfc52d4 100644
--- a/packages/frontend/src/components/global/RouterView.vue
+++ b/packages/frontend/src/components/global/RouterView.vue
@@ -27,6 +27,7 @@ import MkLoadingPage from '@/pages/_loading_.vue';
 
 const props = defineProps<{
 	router?: IRouter;
+	nested?: boolean;
 }>();
 
 const router = props.router ?? inject('router');
@@ -39,6 +40,8 @@ const currentDepth = inject('routerCurrentDepth', 0);
 provide('routerCurrentDepth', currentDepth + 1);
 
 function resolveNested(current: Resolved, d = 0): Resolved | null {
+	if (!props.nested) return current;
+
 	if (d === currentDepth) {
 		return current;
 	} else {
diff --git a/packages/frontend/src/pages/admin/abuses.vue b/packages/frontend/src/pages/admin/abuses.vue
index 0b9847fed3..33021ae025 100644
--- a/packages/frontend/src/pages/admin/abuses.vue
+++ b/packages/frontend/src/pages/admin/abuses.vue
@@ -44,8 +44,10 @@ SPDX-License-Identifier: AGPL-3.0-only
 			</div>
 			-->
 
-			<MkPagination v-slot="{items}" ref="reports" :pagination="pagination" style="margin-top: var(--margin);">
-				<XAbuseReport v-for="report in items" :key="report.id" :report="report" @resolved="resolved"/>
+			<MkPagination v-slot="{items}" ref="reports" :pagination="pagination">
+				<div class="_gaps">
+					<XAbuseReport v-for="report in items" :key="report.id" :report="report" @resolved="resolved"/>
+				</div>
 			</MkPagination>
 		</div>
 	</MkSpacer>
diff --git a/packages/frontend/src/pages/admin/index.vue b/packages/frontend/src/pages/admin/index.vue
index db87bd996d..61745e0ff3 100644
--- a/packages/frontend/src/pages/admin/index.vue
+++ b/packages/frontend/src/pages/admin/index.vue
@@ -25,7 +25,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 		</MkSpacer>
 	</div>
 	<div v-if="!(narrow && currentPage?.route.name == null)" class="main">
-		<RouterView/>
+		<RouterView nested/>
 	</div>
 </div>
 </template>
diff --git a/packages/frontend/src/pages/settings/index.vue b/packages/frontend/src/pages/settings/index.vue
index 7d16740a3e..96a95f1635 100644
--- a/packages/frontend/src/pages/settings/index.vue
+++ b/packages/frontend/src/pages/settings/index.vue
@@ -17,7 +17,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 				</div>
 				<div v-if="!(narrow && currentPage?.route.name == null)" class="main">
 					<div class="bkzroven" style="container-type: inline-size;">
-						<RouterView/>
+						<RouterView nested/>
 					</div>
 				</div>
 			</div>

From 864327b4a7c59d79b5ba17459c795f007c110e82 Mon Sep 17 00:00:00 2001
From: syuilo <4439005+syuilo@users.noreply.github.com>
Date: Fri, 4 Oct 2024 11:20:56 +0900
Subject: [PATCH 027/121] update deps

---
 packages/backend/package.json        |   6 +-
 packages/frontend-embed/package.json |  12 +-
 packages/frontend/package.json       |  50 +-
 pnpm-lock.yaml                       | 947 +++++++++++++++------------
 4 files changed, 553 insertions(+), 462 deletions(-)

diff --git a/packages/backend/package.json b/packages/backend/package.json
index bd5dab618a..c6e31797f8 100644
--- a/packages/backend/package.json
+++ b/packages/backend/package.json
@@ -101,7 +101,7 @@
 		"bcryptjs": "2.4.3",
 		"blurhash": "2.0.5",
 		"body-parser": "1.20.3",
-		"bullmq": "5.13.2",
+		"bullmq": "5.15.0",
 		"cacheable-lookup": "7.0.0",
 		"cbor": "9.0.2",
 		"chalk": "5.3.0",
@@ -166,7 +166,7 @@
 		"rename": "1.0.4",
 		"rss-parser": "3.13.0",
 		"rxjs": "7.8.1",
-		"sanitize-html": "2.13.0",
+		"sanitize-html": "2.13.1",
 		"secure-json-parse": "2.7.0",
 		"sharp": "0.33.5",
 		"slacc": "0.0.10",
@@ -194,7 +194,7 @@
 		"@types/archiver": "6.0.2",
 		"@types/bcryptjs": "2.4.6",
 		"@types/body-parser": "1.19.5",
-		"@types/color-convert": "2.0.3",
+		"@types/color-convert": "2.0.4",
 		"@types/content-disposition": "0.5.8",
 		"@types/fluent-ffmpeg": "2.1.26",
 		"@types/htmlescape": "1.1.3",
diff --git a/packages/frontend-embed/package.json b/packages/frontend-embed/package.json
index 9e720b9835..cb62191c3b 100644
--- a/packages/frontend-embed/package.json
+++ b/packages/frontend-embed/package.json
@@ -18,7 +18,7 @@
 		"@tabler/icons-webfont": "3.3.0",
 		"@twemoji/parser": "15.1.1",
 		"@vitejs/plugin-vue": "5.1.4",
-		"@vue/compiler-sfc": "3.5.10",
+		"@vue/compiler-sfc": "3.5.11",
 		"astring": "1.9.0",
 		"buraha": "0.0.1",
 		"estree-walker": "3.0.3",
@@ -27,8 +27,8 @@
 		"frontend-shared": "workspace:*",
 		"punycode": "2.3.1",
 		"rollup": "4.22.5",
-		"sass": "1.79.3",
-		"shiki": "1.12.0",
+		"sass": "1.79.4",
+		"shiki": "1.21.0",
 		"tinycolor2": "1.6.0",
 		"tsc-alias": "1.8.10",
 		"tsconfig-paths": "4.2.0",
@@ -36,7 +36,7 @@
 		"uuid": "10.0.0",
 		"json5": "2.2.3",
 		"vite": "5.4.8",
-		"vue": "3.5.10"
+		"vue": "3.5.11"
 	},
 	"devDependencies": {
 		"@misskey-dev/summaly": "5.1.0",
@@ -51,10 +51,10 @@
 		"@typescript-eslint/eslint-plugin": "7.17.0",
 		"@typescript-eslint/parser": "7.17.0",
 		"@vitest/coverage-v8": "1.6.0",
-		"@vue/runtime-core": "3.5.10",
+		"@vue/runtime-core": "3.5.11",
 		"acorn": "8.12.1",
 		"cross-env": "7.0.3",
-		"eslint-plugin-import": "2.30.0",
+		"eslint-plugin-import": "2.31.0",
 		"eslint-plugin-vue": "9.28.0",
 		"fast-glob": "3.3.2",
 		"happy-dom": "10.0.3",
diff --git a/packages/frontend/package.json b/packages/frontend/package.json
index 02878c64d9..11d7ff3963 100644
--- a/packages/frontend/package.json
+++ b/packages/frontend/package.json
@@ -28,7 +28,7 @@
 		"@tabler/icons-webfont": "3.3.0",
 		"@twemoji/parser": "15.1.1",
 		"@vitejs/plugin-vue": "5.1.4",
-		"@vue/compiler-sfc": "3.5.10",
+		"@vue/compiler-sfc": "3.5.11",
 		"aiscript-vscode": "github:aiscript-dev/aiscript-vscode#v0.1.11",
 		"astring": "1.9.0",
 		"broadcast-channel": "7.0.0",
@@ -39,7 +39,7 @@
 		"chartjs-chart-matrix": "2.0.1",
 		"chartjs-plugin-gradient": "0.6.1",
 		"chartjs-plugin-zoom": "2.0.1",
-		"chromatic": "11.10.4",
+		"chromatic": "11.11.0",
 		"compare-versions": "6.1.1",
 		"cropperjs": "2.0.0-rc.2",
 		"date-fns": "2.30.0",
@@ -58,7 +58,7 @@
 		"photoswipe": "5.4.4",
 		"punycode": "2.3.1",
 		"rollup": "4.22.5",
-		"sanitize-html": "2.13.0",
+		"sanitize-html": "2.13.1",
 		"sass": "1.79.3",
 		"shiki": "1.21.0",
 		"strict-event-emitter-types": "2.0.0",
@@ -72,29 +72,29 @@
 		"uuid": "10.0.0",
 		"v-code-diff": "1.13.1",
 		"vite": "5.4.8",
-		"vue": "3.5.10",
+		"vue": "3.5.11",
 		"vuedraggable": "next"
 	},
 	"devDependencies": {
 		"@misskey-dev/summaly": "5.1.0",
-		"@storybook/addon-actions": "8.3.3",
-		"@storybook/addon-essentials": "8.3.3",
-		"@storybook/addon-interactions": "8.3.3",
-		"@storybook/addon-links": "8.3.3",
-		"@storybook/addon-mdx-gfm": "8.3.3",
-		"@storybook/addon-storysource": "8.3.3",
-		"@storybook/blocks": "8.3.3",
-		"@storybook/components": "8.3.3",
-		"@storybook/core-events": "8.3.3",
-		"@storybook/manager-api": "8.3.3",
-		"@storybook/preview-api": "8.3.3",
-		"@storybook/react": "8.3.3",
-		"@storybook/react-vite": "8.3.3",
-		"@storybook/test": "8.3.3",
-		"@storybook/theming": "8.3.3",
-		"@storybook/types": "8.3.3",
-		"@storybook/vue3": "8.3.3",
-		"@storybook/vue3-vite": "8.3.3",
+		"@storybook/addon-actions": "8.3.4",
+		"@storybook/addon-essentials": "8.3.4",
+		"@storybook/addon-interactions": "8.3.4",
+		"@storybook/addon-links": "8.3.4",
+		"@storybook/addon-mdx-gfm": "8.3.4",
+		"@storybook/addon-storysource": "8.3.4",
+		"@storybook/blocks": "8.3.4",
+		"@storybook/components": "8.3.4",
+		"@storybook/core-events": "8.3.4",
+		"@storybook/manager-api": "8.3.4",
+		"@storybook/preview-api": "8.3.4",
+		"@storybook/react": "8.3.4",
+		"@storybook/react-vite": "8.3.4",
+		"@storybook/test": "8.3.4",
+		"@storybook/theming": "8.3.4",
+		"@storybook/types": "8.3.4",
+		"@storybook/vue3": "8.3.4",
+		"@storybook/vue3-vite": "8.3.4",
 		"@testing-library/vue": "8.1.0",
 		"@types/estree": "1.0.6",
 		"@types/matter-js": "0.19.7",
@@ -110,11 +110,11 @@
 		"@typescript-eslint/eslint-plugin": "7.17.0",
 		"@typescript-eslint/parser": "7.17.0",
 		"@vitest/coverage-v8": "1.6.0",
-		"@vue/runtime-core": "3.5.10",
+		"@vue/runtime-core": "3.5.11",
 		"acorn": "8.12.1",
 		"cross-env": "7.0.3",
 		"cypress": "13.15.0",
-		"eslint-plugin-import": "2.30.0",
+		"eslint-plugin-import": "2.31.0",
 		"eslint-plugin-vue": "9.28.0",
 		"fast-glob": "3.3.2",
 		"happy-dom": "10.0.3",
@@ -128,7 +128,7 @@
 		"react-dom": "18.3.1",
 		"seedrandom": "3.0.5",
 		"start-server-and-test": "2.0.8",
-		"storybook": "8.3.3",
+		"storybook": "8.3.4",
 		"storybook-addon-misskey-theme": "github:misskey-dev/storybook-addon-misskey-theme",
 		"vite-plugin-turbosnap": "1.0.3",
 		"vitest": "1.6.0",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 53d5dbbde0..b21a74cf57 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -52,7 +52,7 @@ importers:
     devDependencies:
       '@misskey-dev/eslint-plugin':
         specifier: 2.0.3
-        version: 2.0.3(@eslint/compat@1.1.1)(@typescript-eslint/eslint-plugin@7.17.0(@typescript-eslint/parser@7.17.0(eslint@9.8.0)(typescript@5.6.2))(eslint@9.8.0)(typescript@5.6.2))(@typescript-eslint/parser@7.17.0(eslint@9.8.0)(typescript@5.6.2))(eslint-plugin-import@2.30.0(@typescript-eslint/parser@7.17.0(eslint@9.8.0)(typescript@5.6.2))(eslint@9.8.0))(eslint@9.8.0)(globals@15.9.0)
+        version: 2.0.3(@eslint/compat@1.1.1)(@typescript-eslint/eslint-plugin@7.17.0(@typescript-eslint/parser@7.17.0(eslint@9.8.0)(typescript@5.6.2))(eslint@9.8.0)(typescript@5.6.2))(@typescript-eslint/parser@7.17.0(eslint@9.8.0)(typescript@5.6.2))(eslint-plugin-import@2.31.0(@typescript-eslint/parser@7.17.0(eslint@9.8.0)(typescript@5.6.2))(eslint@9.8.0))(eslint@9.8.0)(globals@15.9.0)
       '@types/node':
         specifier: 20.14.12
         version: 20.14.12
@@ -192,8 +192,8 @@ importers:
         specifier: 1.20.3
         version: 1.20.3
       bullmq:
-        specifier: 5.13.2
-        version: 5.13.2
+        specifier: 5.15.0
+        version: 5.15.0
       cacheable-lookup:
         specifier: 7.0.0
         version: 7.0.0
@@ -387,8 +387,8 @@ importers:
         specifier: 7.8.1
         version: 7.8.1
       sanitize-html:
-        specifier: 2.13.0
-        version: 2.13.0
+        specifier: 2.13.1
+        version: 2.13.1
       secure-json-parse:
         specifier: 2.7.0
         version: 2.7.0
@@ -554,8 +554,8 @@ importers:
         specifier: 1.19.5
         version: 1.19.5
       '@types/color-convert':
-        specifier: 2.0.3
-        version: 2.0.3
+        specifier: 2.0.4
+        version: 2.0.4
       '@types/content-disposition':
         specifier: 0.5.8
         version: 0.5.8
@@ -723,10 +723,10 @@ importers:
         version: 15.1.1
       '@vitejs/plugin-vue':
         specifier: 5.1.4
-        version: 5.1.4(vite@5.4.8(@types/node@20.14.12)(sass@1.79.3)(terser@5.33.0))(vue@3.5.10(typescript@5.6.2))
+        version: 5.1.4(vite@5.4.8(@types/node@20.14.12)(sass@1.79.3)(terser@5.33.0))(vue@3.5.11(typescript@5.6.2))
       '@vue/compiler-sfc':
-        specifier: 3.5.10
-        version: 3.5.10
+        specifier: 3.5.11
+        version: 3.5.11
       aiscript-vscode:
         specifier: github:aiscript-dev/aiscript-vscode#v0.1.11
         version: https://codeload.github.com/aiscript-dev/aiscript-vscode/tar.gz/e1e1b27f2f72cd28a473e004b6da0d8fc0bd40d9
@@ -758,8 +758,8 @@ importers:
         specifier: 2.0.1
         version: 2.0.1(chart.js@4.4.4)
       chromatic:
-        specifier: 11.10.4
-        version: 11.10.4
+        specifier: 11.11.0
+        version: 11.11.0
       compare-versions:
         specifier: 6.1.1
         version: 6.1.1
@@ -815,8 +815,8 @@ importers:
         specifier: 4.22.5
         version: 4.22.5
       sanitize-html:
-        specifier: 2.13.0
-        version: 2.13.0
+        specifier: 2.13.1
+        version: 2.13.1
       sass:
         specifier: 1.79.3
         version: 1.79.3
@@ -852,77 +852,77 @@ importers:
         version: 10.0.0
       v-code-diff:
         specifier: 1.13.1
-        version: 1.13.1(vue@3.5.10(typescript@5.6.2))
+        version: 1.13.1(vue@3.5.11(typescript@5.6.2))
       vite:
         specifier: 5.4.8
         version: 5.4.8(@types/node@20.14.12)(sass@1.79.3)(terser@5.33.0)
       vue:
-        specifier: 3.5.10
-        version: 3.5.10(typescript@5.6.2)
+        specifier: 3.5.11
+        version: 3.5.11(typescript@5.6.2)
       vuedraggable:
         specifier: next
-        version: 4.1.0(vue@3.5.10(typescript@5.6.2))
+        version: 4.1.0(vue@3.5.11(typescript@5.6.2))
     devDependencies:
       '@misskey-dev/summaly':
         specifier: 5.1.0
         version: 5.1.0
       '@storybook/addon-actions':
-        specifier: 8.3.3
-        version: 8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))
+        specifier: 8.3.4
+        version: 8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))
       '@storybook/addon-essentials':
-        specifier: 8.3.3
-        version: 8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))
+        specifier: 8.3.4
+        version: 8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))
       '@storybook/addon-interactions':
-        specifier: 8.3.3
-        version: 8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))
+        specifier: 8.3.4
+        version: 8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))
       '@storybook/addon-links':
-        specifier: 8.3.3
-        version: 8.3.3(react@18.3.1)(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))
+        specifier: 8.3.4
+        version: 8.3.4(react@18.3.1)(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))
       '@storybook/addon-mdx-gfm':
-        specifier: 8.3.3
-        version: 8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))
+        specifier: 8.3.4
+        version: 8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))
       '@storybook/addon-storysource':
-        specifier: 8.3.3
-        version: 8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))
+        specifier: 8.3.4
+        version: 8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))
       '@storybook/blocks':
-        specifier: 8.3.3
-        version: 8.3.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))
+        specifier: 8.3.4
+        version: 8.3.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))
       '@storybook/components':
-        specifier: 8.3.3
-        version: 8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))
+        specifier: 8.3.4
+        version: 8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))
       '@storybook/core-events':
-        specifier: 8.3.3
-        version: 8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))
+        specifier: 8.3.4
+        version: 8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))
       '@storybook/manager-api':
-        specifier: 8.3.3
-        version: 8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))
+        specifier: 8.3.4
+        version: 8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))
       '@storybook/preview-api':
-        specifier: 8.3.3
-        version: 8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))
+        specifier: 8.3.4
+        version: 8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))
       '@storybook/react':
-        specifier: 8.3.3
-        version: 8.3.3(@storybook/test@8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4)))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))(typescript@5.6.2)
+        specifier: 8.3.4
+        version: 8.3.4(@storybook/test@8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4)))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))(typescript@5.6.2)
       '@storybook/react-vite':
-        specifier: 8.3.3
-        version: 8.3.3(@storybook/test@8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4)))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@4.22.5)(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))(typescript@5.6.2)(vite@5.4.8(@types/node@20.14.12)(sass@1.79.3)(terser@5.33.0))
+        specifier: 8.3.4
+        version: 8.3.4(@storybook/test@8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4)))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@4.22.5)(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))(typescript@5.6.2)(vite@5.4.8(@types/node@20.14.12)(sass@1.79.3)(terser@5.33.0))
       '@storybook/test':
-        specifier: 8.3.3
-        version: 8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))
+        specifier: 8.3.4
+        version: 8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))
       '@storybook/theming':
-        specifier: 8.3.3
-        version: 8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))
+        specifier: 8.3.4
+        version: 8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))
       '@storybook/types':
-        specifier: 8.3.3
-        version: 8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))
+        specifier: 8.3.4
+        version: 8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))
       '@storybook/vue3':
-        specifier: 8.3.3
-        version: 8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))(vue@3.5.10(typescript@5.6.2))
+        specifier: 8.3.4
+        version: 8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))(vue@3.5.11(typescript@5.6.2))
       '@storybook/vue3-vite':
-        specifier: 8.3.3
-        version: 8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))(vite@5.4.8(@types/node@20.14.12)(sass@1.79.3)(terser@5.33.0))(vue@3.5.10(typescript@5.6.2))
+        specifier: 8.3.4
+        version: 8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))(vite@5.4.8(@types/node@20.14.12)(sass@1.79.3)(terser@5.33.0))(vue@3.5.11(typescript@5.6.2))
       '@testing-library/vue':
         specifier: 8.1.0
-        version: 8.1.0(@vue/compiler-sfc@3.5.10)(@vue/server-renderer@3.5.10(vue@3.5.10(typescript@5.6.2)))(vue@3.5.10(typescript@5.6.2))
+        version: 8.1.0(@vue/compiler-sfc@3.5.11)(@vue/server-renderer@3.5.11(vue@3.5.11(typescript@5.6.2)))(vue@3.5.11(typescript@5.6.2))
       '@types/estree':
         specifier: 1.0.6
         version: 1.0.6
@@ -966,8 +966,8 @@ importers:
         specifier: 1.6.0
         version: 1.6.0(vitest@1.6.0(@types/node@20.14.12)(happy-dom@10.0.3)(jsdom@24.1.1(bufferutil@4.0.8)(utf-8-validate@6.0.4))(sass@1.79.3)(terser@5.33.0))
       '@vue/runtime-core':
-        specifier: 3.5.10
-        version: 3.5.10
+        specifier: 3.5.11
+        version: 3.5.11
       acorn:
         specifier: 8.12.1
         version: 8.12.1
@@ -978,8 +978,8 @@ importers:
         specifier: 13.15.0
         version: 13.15.0
       eslint-plugin-import:
-        specifier: 2.30.0
-        version: 2.30.0(@typescript-eslint/parser@7.17.0(eslint@9.11.0)(typescript@5.6.2))(eslint@9.11.0)
+        specifier: 2.31.0
+        version: 2.31.0(@typescript-eslint/parser@7.17.0(eslint@9.11.0)(typescript@5.6.2))(eslint@9.11.0)
       eslint-plugin-vue:
         specifier: 9.28.0
         version: 9.28.0(eslint@9.11.0)
@@ -1020,11 +1020,11 @@ importers:
         specifier: 2.0.8
         version: 2.0.8
       storybook:
-        specifier: 8.3.3
-        version: 8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4)
+        specifier: 8.3.4
+        version: 8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4)
       storybook-addon-misskey-theme:
         specifier: github:misskey-dev/storybook-addon-misskey-theme
-        version: https://codeload.github.com/misskey-dev/storybook-addon-misskey-theme/tar.gz/cf583db098365b2ccc81a82f63ca9c93bc32b640(@storybook/blocks@8.3.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4)))(@storybook/components@8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4)))(@storybook/core-events@8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4)))(@storybook/manager-api@8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4)))(@storybook/preview-api@8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4)))(@storybook/theming@8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4)))(@storybook/types@8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4)))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+        version: https://codeload.github.com/misskey-dev/storybook-addon-misskey-theme/tar.gz/cf583db098365b2ccc81a82f63ca9c93bc32b640(@storybook/blocks@8.3.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4)))(@storybook/components@8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4)))(@storybook/core-events@8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4)))(@storybook/manager-api@8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4)))(@storybook/preview-api@8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4)))(@storybook/theming@8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4)))(@storybook/types@8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4)))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
       vite-plugin-turbosnap:
         specifier: 1.0.3
         version: 1.0.3
@@ -1066,10 +1066,10 @@ importers:
         version: 15.1.1
       '@vitejs/plugin-vue':
         specifier: 5.1.4
-        version: 5.1.4(vite@5.4.8(@types/node@20.14.12)(sass@1.79.3)(terser@5.33.0))(vue@3.5.10(typescript@5.6.2))
+        version: 5.1.4(vite@5.4.8(@types/node@20.14.12)(sass@1.79.4)(terser@5.33.0))(vue@3.5.11(typescript@5.6.2))
       '@vue/compiler-sfc':
-        specifier: 3.5.10
-        version: 3.5.10
+        specifier: 3.5.11
+        version: 3.5.11
       astring:
         specifier: 1.9.0
         version: 1.9.0
@@ -1098,11 +1098,11 @@ importers:
         specifier: 4.22.5
         version: 4.22.5
       sass:
-        specifier: 1.79.3
-        version: 1.79.3
+        specifier: 1.79.4
+        version: 1.79.4
       shiki:
-        specifier: 1.12.0
-        version: 1.12.0
+        specifier: 1.21.0
+        version: 1.21.0
       tinycolor2:
         specifier: 1.6.0
         version: 1.6.0
@@ -1120,17 +1120,17 @@ importers:
         version: 10.0.0
       vite:
         specifier: 5.4.8
-        version: 5.4.8(@types/node@20.14.12)(sass@1.79.3)(terser@5.33.0)
+        version: 5.4.8(@types/node@20.14.12)(sass@1.79.4)(terser@5.33.0)
       vue:
-        specifier: 3.5.10
-        version: 3.5.10(typescript@5.6.2)
+        specifier: 3.5.11
+        version: 3.5.11(typescript@5.6.2)
     devDependencies:
       '@misskey-dev/summaly':
         specifier: 5.1.0
         version: 5.1.0
       '@testing-library/vue':
         specifier: 8.1.0
-        version: 8.1.0(@vue/compiler-sfc@3.5.10)(@vue/server-renderer@3.5.10(vue@3.5.10(typescript@5.6.2)))(vue@3.5.10(typescript@5.6.2))
+        version: 8.1.0(@vue/compiler-sfc@3.5.11)(@vue/server-renderer@3.5.11(vue@3.5.11(typescript@5.6.2)))(vue@3.5.11(typescript@5.6.2))
       '@types/estree':
         specifier: 1.0.6
         version: 1.0.6
@@ -1160,10 +1160,10 @@ importers:
         version: 7.17.0(eslint@9.11.0)(typescript@5.6.2)
       '@vitest/coverage-v8':
         specifier: 1.6.0
-        version: 1.6.0(vitest@1.6.0(@types/node@20.14.12)(happy-dom@10.0.3)(jsdom@24.1.1)(sass@1.79.3)(terser@5.33.0))
+        version: 1.6.0(vitest@1.6.0(@types/node@20.14.12)(happy-dom@10.0.3)(jsdom@24.1.1)(sass@1.79.4)(terser@5.33.0))
       '@vue/runtime-core':
-        specifier: 3.5.10
-        version: 3.5.10
+        specifier: 3.5.11
+        version: 3.5.11
       acorn:
         specifier: 8.12.1
         version: 8.12.1
@@ -1171,8 +1171,8 @@ importers:
         specifier: 7.0.3
         version: 7.0.3
       eslint-plugin-import:
-        specifier: 2.30.0
-        version: 2.30.0(@typescript-eslint/parser@7.17.0(eslint@9.11.0)(typescript@5.6.2))(eslint@9.11.0)
+        specifier: 2.31.0
+        version: 2.31.0(@typescript-eslint/parser@7.17.0(eslint@9.11.0)(typescript@5.6.2))(eslint@9.11.0)
       eslint-plugin-vue:
         specifier: 9.28.0
         version: 9.28.0(eslint@9.11.0)
@@ -3697,9 +3697,6 @@ packages:
     resolution: {integrity: sha512-+1I5H8dojURiEUGPliDwheQk8dhjp8uV1sMccR/W/zjFrt4wZyPs+Ttp/V7gzm9LDJoNek9tmELert/jQqWTgg==}
     engines: {node: '>=14.18'}
 
-  '@shikijs/core@1.12.0':
-    resolution: {integrity: sha512-mc1cLbm6UQ8RxLc0dZES7v5rkH+99LxQp/ZvTqV3NLyYsO/fD6JhEflP1H5b2SDq9gI0+0G36AVZWxvounfR9w==}
-
   '@shikijs/core@1.21.0':
     resolution: {integrity: sha512-zAPMJdiGuqXpZQ+pWNezQAk5xhzRXBNiECFPcJLtUdsFM3f//G95Z15EHTnHchYycU8kIIysqGgxp8OVSj1SPQ==}
 
@@ -4001,97 +3998,97 @@ packages:
   '@sqltools/formatter@1.2.5':
     resolution: {integrity: sha512-Uy0+khmZqUrUGm5dmMqVlnvufZRSK0FbYzVgp0UMstm+F5+W2/jnEEQyc9vo1ZR/E5ZI/B1WjjoTqBqwJL6Krw==}
 
-  '@storybook/addon-actions@8.3.3':
-    resolution: {integrity: sha512-cbpksmld7iADwDGXgojZ4r8LGI3YA3NP68duAHg2n1dtnx1oUaFK5wd6dbNuz7GdjyhIOIy3OKU1dAuylYNGOQ==}
+  '@storybook/addon-actions@8.3.4':
+    resolution: {integrity: sha512-1y0yD3upKcyzNwwA6loAGW2cRDqExwl4oAT7GJQA4tmabI+fNwmANSgU/ezLvvSUf4Qo0eJHg2Zcn8y+Apq2eA==}
     peerDependencies:
-      storybook: ^8.3.3
+      storybook: ^8.3.4
 
-  '@storybook/addon-backgrounds@8.3.3':
-    resolution: {integrity: sha512-aX0OIrtjIB7UgSaiv20SFkfC1iWwJIGMPsPSJ5ZPhXIIOWIEBtSujh8YXwjDEXSC4DOHalmeT4bitRRe5KrVKA==}
+  '@storybook/addon-backgrounds@8.3.4':
+    resolution: {integrity: sha512-o3nl7cN3x8erJNxLEv8YptanEQAnbqnaseOAsvSC6/nnSAcRYBSs3BvekKvo4CcpS2mxn7F5NJTBFYnCXzy8EA==}
     peerDependencies:
-      storybook: ^8.3.3
+      storybook: ^8.3.4
 
-  '@storybook/addon-controls@8.3.3':
-    resolution: {integrity: sha512-78xRtVpY7eX/Lti00JLgwYCBRB6ZcvzY3SWk0uQjEqcTnQGoQkVg2L7oWFDlDoA1LBY18P5ei2vu8MYT9GXU4g==}
+  '@storybook/addon-controls@8.3.4':
+    resolution: {integrity: sha512-qQcaK6dczsb6wXkzGZKOjUYNA7FfKBewRv6NvoVKYY6LfhllGOkmUAtYpdtQG8adsZWTSoZaAOJS2vP2uM67lw==}
     peerDependencies:
-      storybook: ^8.3.3
+      storybook: ^8.3.4
 
-  '@storybook/addon-docs@8.3.3':
-    resolution: {integrity: sha512-REUandqq1RnMNOhsocRwx5q2fdlBAYPTDFlKASYfEn4Ln5NgbQRGxOAWl7yXAAFzbDmUDU7K20hkauecF0tyMw==}
+  '@storybook/addon-docs@8.3.4':
+    resolution: {integrity: sha512-TWauhqF/gJgfwPuWeM6KM3LwC+ErCOM+K2z16w3vgao9s67sij8lnrdAoQ0hjA+kw2/KAdCakFS6FyciG81qog==}
     peerDependencies:
-      storybook: ^8.3.3
+      storybook: ^8.3.4
 
-  '@storybook/addon-essentials@8.3.3':
-    resolution: {integrity: sha512-E/uXoUYcg8ulG3lVbsEKb4v5hnMeGkq9YJqiZYKgVK7iRFa6p4HeVB1wU1adnm7RgjWvh+p0vQRo4KL2CTNXqw==}
+  '@storybook/addon-essentials@8.3.4':
+    resolution: {integrity: sha512-C3+3hpmSn/8zdx5sXEP0eE6zMzxgRosHVZYfe9nBcMiEDp6UKVUyHVetWxEULOEgN46ysjcpllZ0bUkRYxi2IQ==}
     peerDependencies:
-      storybook: ^8.3.3
+      storybook: ^8.3.4
 
-  '@storybook/addon-highlight@8.3.3':
-    resolution: {integrity: sha512-MB084xJM66rLU+iFFk34kjLUiAWzDiy6Kz4uZRa1CnNqEK0sdI8HaoQGgOxTIa2xgJor05/8/mlYlMkP/0INsQ==}
+  '@storybook/addon-highlight@8.3.4':
+    resolution: {integrity: sha512-rxZTeuZyZ7RnU+xmRhS01COFLbGnVEmlUNxBw8ArsrTEZKW5PbKpIxNLTj9F0zdH8H0MfryJGP+Aadcm0oHWlw==}
     peerDependencies:
-      storybook: ^8.3.3
+      storybook: ^8.3.4
 
-  '@storybook/addon-interactions@8.3.3':
-    resolution: {integrity: sha512-3w5tpCGYdF33wF44xEhTS3Zmcwd6nITtwy5q+PJvHCJAm3fpjzL3xrjtlHKDvXNwYacJPRCbWKn2QwtxZIdN0g==}
+  '@storybook/addon-interactions@8.3.4':
+    resolution: {integrity: sha512-ORxqe35wUmF7EDHo45mdDHiju3Ryk2pZ1vO9PyvW6ZItNlHt/IxAr7T/TysGejZ/eTBg6tMZR3ExGky3lTg/CQ==}
     peerDependencies:
-      storybook: ^8.3.3
+      storybook: ^8.3.4
 
-  '@storybook/addon-links@8.3.3':
-    resolution: {integrity: sha512-rz4KEbzr1ca4zZEZwbOnhKiaEsokCl1KkngxT/C1YIkpW908j/kg2nnIb5MrtlAW1nirXguAR74t6CGntvdU9w==}
+  '@storybook/addon-links@8.3.4':
+    resolution: {integrity: sha512-R1DjARmxRIKJDGIG6uxmQ1yFNyoQbb+QIPUFjgWCak8+AdLJbC7W+Esvo9F5hQfh6czyy0piiM3qj5hpQJVh3A==}
     peerDependencies:
       react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta
-      storybook: ^8.3.3
+      storybook: ^8.3.4
     peerDependenciesMeta:
       react:
         optional: true
 
-  '@storybook/addon-mdx-gfm@8.3.3':
-    resolution: {integrity: sha512-jdwVXoBSEdmuw8L4MxUeJ/qIInADfCwdtShnfTQIJBBRucOl8ykgfTKKNjllT79TFiK0gsWoiZmE05P4wuBofw==}
+  '@storybook/addon-mdx-gfm@8.3.4':
+    resolution: {integrity: sha512-O0sMP7VFo1fKsdViY+W6OMNYEXvB5FzEEsqgsydMcsJ0qOKR1li2l3cLCMLXdUKVZ+2uRbEhnm2RnB9RWF5O7g==}
     peerDependencies:
-      storybook: ^8.3.3
+      storybook: ^8.3.4
 
-  '@storybook/addon-measure@8.3.3':
-    resolution: {integrity: sha512-R20Z83gnxDRrocES344dw1Of/zDhe3XHSM6TLq80UQTJ9PhnMI+wYHQlK9DsdP3KiRkI+pQA6GCOp0s2ZRy5dg==}
+  '@storybook/addon-measure@8.3.4':
+    resolution: {integrity: sha512-IJ6WKEbqmG+r7sukFjo+bVmPB2Zry04sylGx/OGyOh7zIhhqAqpwOwMHP0uQrc3tLNnUM6qB/o83UyYX79ql+A==}
     peerDependencies:
-      storybook: ^8.3.3
+      storybook: ^8.3.4
 
-  '@storybook/addon-outline@8.3.3':
-    resolution: {integrity: sha512-OwqYfieNuqSqWNtUZLu3UmsfQNnwA2UaSMBZyeC2Dte9Jd59PPYggcWmH+b0S6OTbYXWNAUK5U6WdK+X9Ypzdw==}
+  '@storybook/addon-outline@8.3.4':
+    resolution: {integrity: sha512-kRRJTTLKM8gMfeh/e83djN5XLlc0hFtr9zKWxuZxaXt9Hmr+9tH/PRFtVK/S4SgqnBDoXk49Wgv6raiwj5/e3A==}
     peerDependencies:
-      storybook: ^8.3.3
+      storybook: ^8.3.4
 
-  '@storybook/addon-storysource@8.3.3':
-    resolution: {integrity: sha512-yPYQH9NepSNxoSsV9E7OV3/EVFrbU/r2B3E5WP/mCfqTXPg/5noce7iRi+rWqcVM1tsN1qPnSjfQQc7noF0h0Q==}
+  '@storybook/addon-storysource@8.3.4':
+    resolution: {integrity: sha512-uHTUiK7dzWRZAKpPafBH3U5PWAP7+J97lg66HDKAHpmmQdy7v3HfXaYNX1FoI+PeC5piUxFETXM0z+BNvJCknA==}
     peerDependencies:
-      storybook: ^8.3.3
+      storybook: ^8.3.4
 
-  '@storybook/addon-toolbars@8.3.3':
-    resolution: {integrity: sha512-4WyiVqDm4hlJdENIVQg9pLNLdfhnNKa+haerYYSzTVjzYrUx0X6Bxafshq+sud6aRtSYU14abwP56lfW8hgTlA==}
+  '@storybook/addon-toolbars@8.3.4':
+    resolution: {integrity: sha512-Km1YciVIxqluDbd1xmHjANNFyMonEOtnA6e4MrnBnC9XkPXSigeFlj0JvxyI/zjBsLBoFRmQiwq55W6l3hQ9sA==}
     peerDependencies:
-      storybook: ^8.3.3
+      storybook: ^8.3.4
 
-  '@storybook/addon-viewport@8.3.3':
-    resolution: {integrity: sha512-2S+UpbKAL+z1ppzUCkixjaem2UDMkfmm/kyJ1wm3A/ofGLYi4fjMSKNRckk+7NdolXGQJjBo0RcaotUTxFIFwQ==}
+  '@storybook/addon-viewport@8.3.4':
+    resolution: {integrity: sha512-fU4LdXSSqIOLbCEh2leq/tZUYlFliXZBWr/+igQHdUoU7HY8RIImXqVUaR9wlCaTb48WezAWT60vJtwNijyIiQ==}
     peerDependencies:
-      storybook: ^8.3.3
+      storybook: ^8.3.4
 
-  '@storybook/blocks@8.3.3':
-    resolution: {integrity: sha512-8Vsvxqstop3xfbsx3Dn1nEjyxvQUcOYd8vpxyp2YumxYO8FlXIRuYL6HAkYbcX8JexsKvCZYxor52D2vUGIKZg==}
+  '@storybook/blocks@8.3.4':
+    resolution: {integrity: sha512-1g4aCrd5CcN+pVhF2ATu9ZRVvAIgBMb2yF9KkCuTpdvqKDuDNK3sGb0CxjS7jp3LOvyjJr9laTOQsz8v8MQc5A==}
     peerDependencies:
       react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta
       react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta
-      storybook: ^8.3.3
+      storybook: ^8.3.4
     peerDependenciesMeta:
       react:
         optional: true
       react-dom:
         optional: true
 
-  '@storybook/builder-vite@8.3.3':
-    resolution: {integrity: sha512-3yTXCLaB6bzhoPH3PqtacKkcaC1uV4L+IHTf1Zypx1NO1pLZHyhYf0T7dIOxTh2JZfqu1Pm9hTvOmWfR12m+9w==}
+  '@storybook/builder-vite@8.3.4':
+    resolution: {integrity: sha512-Sa6SZ7LeHpkrnuvua8P8MR8e8a+MPKbyMmr9TqCCy8Ud/t4AM4kHY3JpJGtrgeK9l43fBnBwfdZYoRl5J6oWeA==}
     peerDependencies:
       '@preact/preset-vite': '*'
-      storybook: ^8.3.3
+      storybook: ^8.3.4
       typescript: '>= 4.3.x'
       vite: ^4.0.0 || ^5.0.0
       vite-plugin-glimmerx: '*'
@@ -4103,23 +4100,23 @@ packages:
       vite-plugin-glimmerx:
         optional: true
 
-  '@storybook/components@8.3.3':
-    resolution: {integrity: sha512-i2JYtesFGkdu+Hwuj+o9fLuO3yo+LPT1/8o5xBVYtEqsgDtEAyuRUWjSz8d8NPtzloGPOv5kvR6MokWDfbeMfw==}
+  '@storybook/components@8.3.4':
+    resolution: {integrity: sha512-iQzLJd87uGbFBbYNqlrN/ABrnx3dUrL0tjPCarzglzshZoPCNOsllJeJx5TJwB9kCxSZ8zB9TTOgr7NXl+oyVA==}
     peerDependencies:
-      storybook: ^8.3.3
+      storybook: ^8.3.4
 
-  '@storybook/core-events@8.3.3':
-    resolution: {integrity: sha512-YL+gBuCS81qktzTkvw0MXUJW0bYAXfRzMoiLfDBTrEKZfcJOB4JAlMGmvRRar0+jygK3icD42Rl5BwWoZY6KFQ==}
+  '@storybook/core-events@8.3.4':
+    resolution: {integrity: sha512-3/5oJN2UnlmUILXCh7SXMTa2MYZOvrjeZCm3wFomoQASU2FFzS5AxBYYnwNdtrZmn4w32uw4T7qvA0+96Utwsg==}
     peerDependencies:
-      storybook: ^8.3.3
+      storybook: ^8.3.4
 
-  '@storybook/core@8.3.3':
-    resolution: {integrity: sha512-pmf2bP3fzh45e56gqOuBT8sDX05hGdUKIZ/hcI84d5xmd6MeHiPW8th2v946wCHcxHzxib2/UU9vQUh+mB4VNw==}
+  '@storybook/core@8.3.4':
+    resolution: {integrity: sha512-4PZB91JJpuKfcjeOR2LXj3ABaPLLSd2P/SfYOKNCygrDstsQa/yay3/yN5Z9yi1cIG84KRr6/sUW+0x8HsGLPg==}
 
-  '@storybook/csf-plugin@8.3.3':
-    resolution: {integrity: sha512-7AD7ojpXr3THqpTcEI4K7oKUfSwt1hummgL/cASuQvEPOwAZCVZl2gpGtKxcXhtJXTkn3GMCAvlYMoe7O/1YWw==}
+  '@storybook/csf-plugin@8.3.4':
+    resolution: {integrity: sha512-ZMFWYxeTN4GxCn8dyIH4roECyLDy29yv/QKM+pHM3AC5Ny2HWI35SohWao4fGBAFxPQFbR5hPN8xa6ofHPSSTg==}
     peerDependencies:
-      storybook: ^8.3.3
+      storybook: ^8.3.4
 
   '@storybook/csf@0.1.11':
     resolution: {integrity: sha512-dHYFQH3mA+EtnCkHXzicbLgsvzYjcDJ1JWsogbItZogkPHgSJM/Wr71uMkcvw8v9mmCyP4NpXJuu6bPoVsOnzg==}
@@ -4134,45 +4131,45 @@ packages:
       react: ^16.8.0 || ^17.0.0 || ^18.0.0
       react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
 
-  '@storybook/instrumenter@8.3.3':
-    resolution: {integrity: sha512-ZiODB9EwCQkl4PBxGJjBHXRTLxcNs68ZZvR+xeMr0eMFzzlJG+trXoX5kK95oA4BFhGN+3uM0Zl3MoRjBtJTNA==}
+  '@storybook/instrumenter@8.3.4':
+    resolution: {integrity: sha512-jVhfNOPekOyJmta0BTkQl9Z6rgRbFHlc0eV4z1oSrzaawSlc9TFzAeDCtCP57vg3FuBX8ydDYAvyZ7s4xPpLyg==}
     peerDependencies:
-      storybook: ^8.3.3
+      storybook: ^8.3.4
 
-  '@storybook/manager-api@8.3.3':
-    resolution: {integrity: sha512-Na4U+McOeVUJAR6qzJfQ6y2Qt0kUgEDUriNoAn+curpoKPTmIaZ79RAXBzIqBl31VyQKknKpZbozoRGf861YaQ==}
+  '@storybook/manager-api@8.3.4':
+    resolution: {integrity: sha512-tBx7MBfPUrKSlD666zmVjtIvoNArwCciZiW/UJ8IWmomrTJRfFBnVvPVM2gp1lkDIzRHYmz5x9BHbYaEDNcZWQ==}
     peerDependencies:
-      storybook: ^8.3.3
+      storybook: ^8.3.4
 
-  '@storybook/preview-api@8.3.3':
-    resolution: {integrity: sha512-GP2QlaF3BBQGAyo248N7549YkTQjCentsc1hUvqPnFWU4xfjkejbnFk8yLaIw0VbYbL7jfd7npBtjZ+6AnphMQ==}
+  '@storybook/preview-api@8.3.4':
+    resolution: {integrity: sha512-/YKQ3QDVSHmtFXXCShf5w0XMlg8wkfTpdYxdGv1CKFV8DU24f3N7KWulAgeWWCWQwBzZClDa9kzxmroKlQqx3A==}
     peerDependencies:
-      storybook: ^8.3.3
+      storybook: ^8.3.4
 
-  '@storybook/react-dom-shim@8.3.3':
-    resolution: {integrity: sha512-0dPC9K7+K5+X/bt3GwYmh+pCpisUyKVjWsI+PkzqGnWqaXFakzFakjswowIAIO1rf7wYZR591x3ehUAyL2bJiQ==}
+  '@storybook/react-dom-shim@8.3.4':
+    resolution: {integrity: sha512-L4llDvjaAzqPx6h4ddZMh36wPr75PrI2S8bXy+flLqAeVRYnRt4WNKGuxqH0t0U6MwId9+vlCZ13JBfFuY7eQQ==}
     peerDependencies:
       react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta
       react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta
-      storybook: ^8.3.3
+      storybook: ^8.3.4
 
-  '@storybook/react-vite@8.3.3':
-    resolution: {integrity: sha512-vzOqVaA/rv+X5J17eWKxdZztMKEKfsCSP8pNNmrqXWxK3pSlW0fAPxtn1kw3UNxGtAv71pcqvaCUtTJKqI1PYA==}
+  '@storybook/react-vite@8.3.4':
+    resolution: {integrity: sha512-0Xm8eTH+jQ7SV4moLkPN4G6U2IDrqXPXUqsZdXaccepIMcD4G75foQFm2LOrFJuY+IMySPspKeTqf8OLskPppw==}
     engines: {node: '>=18.0.0'}
     peerDependencies:
       react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta
       react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta
-      storybook: ^8.3.3
+      storybook: ^8.3.4
       vite: ^4.0.0 || ^5.0.0
 
-  '@storybook/react@8.3.3':
-    resolution: {integrity: sha512-fHOW/mNqI+sZWttGOE32Q+rAIbN7/Oib091cmE8usOM0z0vPNpywUBtqC2cCQH39vp19bhTsQaSsTcoBSweAHw==}
+  '@storybook/react@8.3.4':
+    resolution: {integrity: sha512-PA7iQL4/9X2/iLrv+AUPNtlhTHJWhDao9gQIT1Hef39FtFk+TU9lZGbv+g29R1H9V3cHP5162nG2aTu395kmbA==}
     engines: {node: '>=18.0.0'}
     peerDependencies:
-      '@storybook/test': 8.3.3
+      '@storybook/test': 8.3.4
       react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta
       react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta
-      storybook: ^8.3.3
+      storybook: ^8.3.4
       typescript: '>= 4.2.x'
     peerDependenciesMeta:
       '@storybook/test':
@@ -4180,38 +4177,38 @@ packages:
       typescript:
         optional: true
 
-  '@storybook/source-loader@8.3.3':
-    resolution: {integrity: sha512-NeP7l53mvnnfwi+91vtRaibZer+UJi6gkoaGRCpphL3L+3qVIXN3p41uXhAy+TahdFI2dbrWvLSNgtsvdXVaFg==}
+  '@storybook/source-loader@8.3.4':
+    resolution: {integrity: sha512-wH//LuWfa2iOmjykSqsub8M8e0EdhEUZoHUFhwBeizfYQQHaMaSEBhhAQCaWWKmdGB9lnCe1cioQ32c2IWtBIw==}
     peerDependencies:
-      storybook: ^8.3.3
+      storybook: ^8.3.4
 
-  '@storybook/test@8.3.3':
-    resolution: {integrity: sha512-uZ8nMIovfI2ry989K2+cYAeEVD/3dpjj2+Rbmy7DiZWWVhFALfmqaTRkzZfShLmlH0TFv+rfcBPihGccBtw0FQ==}
+  '@storybook/test@8.3.4':
+    resolution: {integrity: sha512-HRiUenitln8QPHu6DEWUg9s9cEoiGN79lMykzXzw9shaUvdEIhWCsh82YKtmB3GJPj6qcc6dZL/Aio8srxyGAg==}
     peerDependencies:
-      storybook: ^8.3.3
+      storybook: ^8.3.4
 
-  '@storybook/theming@8.3.3':
-    resolution: {integrity: sha512-gWJKetI6XJQgkrvvry4ez10+jLaGNCQKi5ygRPM9N+qrjA3BB8F2LCuFUTBuisa4l64TILDNjfwP/YTWV5+u5A==}
+  '@storybook/theming@8.3.4':
+    resolution: {integrity: sha512-D4XVsQgTtpHEHLhwkx59aGy1GBwOedVr/mNns7hFrH8FjEpxrrWCuZQASq1ZpCl8LXlh7uvmT5sM2rOdQbGuGg==}
     peerDependencies:
-      storybook: ^8.3.3
+      storybook: ^8.3.4
 
-  '@storybook/types@8.3.3':
-    resolution: {integrity: sha512-wV1kupG1tfTMOXaBrtVHXuqp19vURVDqWTQX6nqkoUFD7Xb1lz/YNVeGP1uT/zJdJy42/HIyoib9JPx9h0Vx9w==}
+  '@storybook/types@8.3.4':
+    resolution: {integrity: sha512-kIyb0g8C6EizI0Mv+l6L6yjCJe9/vW3UvgsZL5BXqs8THTAfs3/+A9Q9jDEMovSIVI3EgesO79+OCEazDUHmOA==}
     peerDependencies:
-      storybook: ^8.3.3
+      storybook: ^8.3.4
 
-  '@storybook/vue3-vite@8.3.3':
-    resolution: {integrity: sha512-IFcoOGlUGuUkL3rpm9UFs8FK9JX1ZdfGpLXRObVOVRhW3t+MsNLpx4Fqp3a/re6WcCC3yvHzbLXgvGcjpapkbw==}
+  '@storybook/vue3-vite@8.3.4':
+    resolution: {integrity: sha512-0H1tLbRd8i6L3EW8QC9bDlgPIUM5i6b7onvyyQhyIxODWRfigHi6UP9sjHfrljdvnlOtYlZT2A5QbpkugzwLjg==}
     engines: {node: '>=18.0.0'}
     peerDependencies:
-      storybook: ^8.3.3
+      storybook: ^8.3.4
       vite: ^4.0.0 || ^5.0.0
 
-  '@storybook/vue3@8.3.3':
-    resolution: {integrity: sha512-peu8MFGwmhpXoD3n42qG6TxeVHRhfHZ0/HW4+A6FXSB1c9w0CC4AzHs5f1w3yUvshtexNN5bkw9Q4nSVKtfU7A==}
+  '@storybook/vue3@8.3.4':
+    resolution: {integrity: sha512-NNQXwidr+QjLndORWtPjXv/obsNNfJhP5Xj6vUZslrDpdIyTL3NEM+ktLK2EMw/a3zUbJMnMkyMgoWvioCNHxQ==}
     engines: {node: '>=18.0.0'}
     peerDependencies:
-      storybook: ^8.3.3
+      storybook: ^8.3.4
       vue: ^3.0.0
 
   '@swc/cli@0.3.12':
@@ -4591,8 +4588,8 @@ packages:
   '@types/cacheable-request@6.0.3':
     resolution: {integrity: sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==}
 
-  '@types/color-convert@2.0.3':
-    resolution: {integrity: sha512-2Q6wzrNiuEvYxVQqhh7sXM2mhIhvZR/Paq4FdsQkOMgWsCIkKvSGj8Le1/XalulrmgOzPMqNa0ix+ePY4hTrfg==}
+  '@types/color-convert@2.0.4':
+    resolution: {integrity: sha512-Ub1MmDdyZ7mX//g25uBAoH/mWGd9swVbt8BseymnaE18SU4po/PjmCrHxqIIRjBo3hV/vh1KGr0eMxUhp+t+dQ==}
 
   '@types/color-name@1.1.1':
     resolution: {integrity: sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==}
@@ -5106,8 +5103,8 @@ packages:
   '@vue/compiler-core@3.5.10':
     resolution: {integrity: sha512-iXWlk+Cg/ag7gLvY0SfVucU8Kh2CjysYZjhhP70w9qI4MvSox4frrP+vDGvtQuzIcgD8+sxM6lZvCtdxGunTAA==}
 
-  '@vue/compiler-core@3.5.7':
-    resolution: {integrity: sha512-A0gay3lK71MddsSnGlBxRPOugIVdACze9L/rCo5X5srCyjQfZOfYtSFMJc3aOZCM+xN55EQpb4R97rYn/iEbSw==}
+  '@vue/compiler-core@3.5.11':
+    resolution: {integrity: sha512-PwAdxs7/9Hc3ieBO12tXzmTD+Ln4qhT/56S+8DvrrZ4kLDn4Z/AMUr8tXJD0axiJBS0RKIoNaR0yMuQB9v9Udg==}
 
   '@vue/compiler-dom@3.4.37':
     resolution: {integrity: sha512-rIiSmL3YrntvgYV84rekAtU/xfogMUJIclUMeIKEtVBFngOL3IeZHhsH3UaFEgB5iFGpj6IW+8YuM/2Up+vVag==}
@@ -5115,20 +5112,20 @@ packages:
   '@vue/compiler-dom@3.5.10':
     resolution: {integrity: sha512-DyxHC6qPcktwYGKOIy3XqnHRrrXyWR2u91AjP+nLkADko380srsC2DC3s7Y1Rk6YfOlxOlvEQKa9XXmLI+W4ZA==}
 
-  '@vue/compiler-dom@3.5.7':
-    resolution: {integrity: sha512-GYWl3+gO8/g0ZdYaJ18fYHdI/WVic2VuuUd1NsPp60DWXKy+XjdhFsDW7FbUto8siYYZcosBGn9yVBkjhq1M8Q==}
+  '@vue/compiler-dom@3.5.11':
+    resolution: {integrity: sha512-pyGf8zdbDDRkBrEzf8p7BQlMKNNF5Fk/Cf/fQ6PiUz9at4OaUfyXW0dGJTo2Vl1f5U9jSLCNf0EZJEogLXoeew==}
 
   '@vue/compiler-sfc@3.4.37':
     resolution: {integrity: sha512-vCfetdas40Wk9aK/WWf8XcVESffsbNkBQwS5t13Y/PcfqKfIwJX2gF+82th6dOpnpbptNMlMjAny80li7TaCIg==}
 
-  '@vue/compiler-sfc@3.5.10':
-    resolution: {integrity: sha512-to8E1BgpakV7224ZCm8gz1ZRSyjNCAWEplwFMWKlzCdP9DkMKhRRwt0WkCjY7jkzi/Vz3xgbpeig5Pnbly4Tow==}
+  '@vue/compiler-sfc@3.5.11':
+    resolution: {integrity: sha512-gsbBtT4N9ANXXepprle+X9YLg2htQk1sqH/qGJ/EApl+dgpUBdTv3yP7YlR535uHZY3n6XaR0/bKo0BgwwDniw==}
 
   '@vue/compiler-ssr@3.4.37':
     resolution: {integrity: sha512-TyAgYBWrHlFrt4qpdACh8e9Ms6C/AZQ6A6xLJaWrCL8GCX5DxMzxyeFAEMfU/VFr4tylHm+a2NpfJpcd7+20XA==}
 
-  '@vue/compiler-ssr@3.5.10':
-    resolution: {integrity: sha512-hxP4Y3KImqdtyUKXDRSxKSRkSm1H9fCvhojEYrnaoWhE4w/y8vwWhnosJoPPe2AXm5sU7CSbYYAgkt2ZPhDz+A==}
+  '@vue/compiler-ssr@3.5.11':
+    resolution: {integrity: sha512-P4+GPjOuC2aFTk1Z4WANvEhyOykcvEd5bIj2KVNGKGfM745LaXGr++5njpdBTzVz5pZifdlR1kpYSJJpIlSePA==}
 
   '@vue/compiler-vue2@2.7.16':
     resolution: {integrity: sha512-qYC3Psj9S/mfu9uVi5WvNZIzq+xnXMhOwbTFKKDD7b1lhpnn71jXSFdTQ+WsIEk0ONCd7VV2IMm7ONl6tbQ86A==}
@@ -5152,30 +5149,30 @@ packages:
   '@vue/reactivity@3.4.37':
     resolution: {integrity: sha512-UmdKXGx0BZ5kkxPqQr3PK3tElz6adTey4307NzZ3whZu19i5VavYal7u2FfOmAzlcDVgE8+X0HZ2LxLb/jgbYw==}
 
-  '@vue/reactivity@3.5.10':
-    resolution: {integrity: sha512-kW08v06F6xPSHhid9DJ9YjOGmwNDOsJJQk0ax21wKaUYzzuJGEuoKNU2Ujux8FLMrP7CFJJKsHhXN9l2WOVi2g==}
+  '@vue/reactivity@3.5.11':
+    resolution: {integrity: sha512-Nqo5VZEn8MJWlCce8XoyVqHZbd5P2NH+yuAaFzuNSR96I+y1cnuUiq7xfSG+kyvLSiWmaHTKP1r3OZY4mMD50w==}
 
   '@vue/runtime-core@3.4.37':
     resolution: {integrity: sha512-MNjrVoLV/sirHZoD7QAilU1Ifs7m/KJv4/84QVbE6nyAZGQNVOa1HGxaOzp9YqCG+GpLt1hNDC4RbH+KtanV7w==}
 
-  '@vue/runtime-core@3.5.10':
-    resolution: {integrity: sha512-9Q86I5Qq3swSkFfzrZ+iqEy7Vla325M7S7xc1NwKnRm/qoi1Dauz0rT6mTMmscqx4qz0EDJ1wjB+A36k7rl8mA==}
+  '@vue/runtime-core@3.5.11':
+    resolution: {integrity: sha512-7PsxFGqwfDhfhh0OcDWBG1DaIQIVOLgkwA5q6MtkPiDFjp5gohVnJEahSktwSFLq7R5PtxDKy6WKURVN1UDbzA==}
 
   '@vue/runtime-dom@3.4.37':
     resolution: {integrity: sha512-Mg2EwgGZqtwKrqdL/FKMF2NEaOHuH+Ks9TQn3DHKyX//hQTYOun+7Tqp1eo0P4Ds+SjltZshOSRq6VsU0baaNg==}
 
-  '@vue/runtime-dom@3.5.10':
-    resolution: {integrity: sha512-t3x7ht5qF8ZRi1H4fZqFzyY2j+GTMTDxRheT+i8M9Ph0oepUxoadmbwlFwMoW7RYCpNQLpP2Yx3feKs+fyBdpA==}
+  '@vue/runtime-dom@3.5.11':
+    resolution: {integrity: sha512-GNghjecT6IrGf0UhuYmpgaOlN7kxzQBhxWEn08c/SQDxv1yy4IXI1bn81JgEpQ4IXjRxWtPyI8x0/7TF5rPfYQ==}
 
   '@vue/server-renderer@3.4.37':
     resolution: {integrity: sha512-jZ5FAHDR2KBq2FsRUJW6GKDOAG9lUTX8aBEGq4Vf6B/35I9fPce66BornuwmqmKgfiSlecwuOb6oeoamYMohkg==}
     peerDependencies:
       vue: 3.4.37
 
-  '@vue/server-renderer@3.5.10':
-    resolution: {integrity: sha512-IVE97tt2kGKwHNq9yVO0xdh1IvYfZCShvDSy46JIh5OQxP1/EXSpoDqetVmyIzL7CYOWnnmMkVqd7YK2QSWkdw==}
+  '@vue/server-renderer@3.5.11':
+    resolution: {integrity: sha512-cVOwYBxR7Wb1B1FoxYvtjJD8X/9E5nlH4VSkJy2uMA1MzYNdzAAB//l8nrmN9py/4aP+3NjWukf9PZ3TeWULaA==}
     peerDependencies:
-      vue: 3.5.10
+      vue: 3.5.11
 
   '@vue/shared@3.4.37':
     resolution: {integrity: sha512-nIh8P2fc3DflG8+5Uw8PT/1i17ccFn0xxN/5oE9RfV5SVnd7G0XEFRwakrnNFE/jlS95fpGXDVG5zDETS26nmg==}
@@ -5183,8 +5180,8 @@ packages:
   '@vue/shared@3.5.10':
     resolution: {integrity: sha512-VkkBhU97Ki+XJ0xvl4C9YJsIZ2uIlQ7HqPpZOS3m9VCvmROPaChZU6DexdMJqvz9tbgG+4EtFVrSuailUq5KGQ==}
 
-  '@vue/shared@3.5.7':
-    resolution: {integrity: sha512-NBE1PBIvzIedxIc2RZiKXvGbJkrZ2/hLf3h8GlS4/sP9xcXEZMFWOazFkNd6aGeUCMaproe5MHVYB3/4AW9q9g==}
+  '@vue/shared@3.5.11':
+    resolution: {integrity: sha512-W8GgysJVnFo81FthhzurdRAWP/byq3q2qIw70e0JWblzVhjgOMiC2GyovXrZTFQJnFVryYaKGP3Tc9vYzYm6PQ==}
 
   '@vue/test-utils@2.4.1':
     resolution: {integrity: sha512-VO8nragneNzUZUah6kOjiFmD/gwRjUauG9DROh6oaOeFwX1cZRUNHhdeogE8635cISigXFTtGLUQWx5KCb0xeg==}
@@ -5665,8 +5662,8 @@ packages:
     resolution: {integrity: sha512-4T53u4PdgsXqKaIctwF8ifXlRTTmEPJ8iEPWFdGZvcf7sbwYo6FKFEX9eNNAnzFZ7EzJAQ3CJeOtCRA4rDp7Pw==}
     engines: {node: '>=6.14.2'}
 
-  bullmq@5.13.2:
-    resolution: {integrity: sha512-McGE8k3mrCvdUHdU0sHkTKDS1xr4pff+hbEKBY51wk5S6Za0gkuejYA620VQTo3Zz37E/NVWMgumwiXPQ3yZcA==}
+  bullmq@5.15.0:
+    resolution: {integrity: sha512-h53shVjx8s6wxYGtUfzAfENpSP7N5T0D4PMTvbZncozLjb8yUKhopfpa7PmcpQfq7SSO9dm/OZ9XQuGOCSGNug==}
 
   buraha@0.0.1:
     resolution: {integrity: sha512-G563A0mTbzknm2jDaNxfZuNKIdeArs8T+XQN6t+KbmgnOoevXSXhKDkyf8Md/36Jrx99ikwbCag37VGe3myExQ==}
@@ -5864,8 +5861,8 @@ packages:
     resolution: {integrity: sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==}
     engines: {node: '>=10'}
 
-  chromatic@11.10.4:
-    resolution: {integrity: sha512-nfgDpW5gQ4FtgV1lZXXfqLjONKDCh2K4vwI3dbZrtU1ObOL9THyAzpIdnK9LRcNSeisDLX+XFCryfMg1Ql2U2g==}
+  chromatic@11.11.0:
+    resolution: {integrity: sha512-mwmYsNMsZlRLtlfFUEtac5zhoVRhc+O/lsuMdOpwkiDQiKX6WdSNIhic+dkLenfuzao2r18s50nphcOgFoatBg==}
     hasBin: true
     peerDependencies:
       '@chromatic-com/cypress': ^0.*.* || ^1.0.0
@@ -6684,6 +6681,27 @@ packages:
       eslint-import-resolver-webpack:
         optional: true
 
+  eslint-module-utils@2.12.0:
+    resolution: {integrity: sha512-wALZ0HFoytlyh/1+4wuZ9FJCD/leWHQzzrxJ8+rebyReSLk7LApMyd3WJaLVoN+D5+WIdJyDK1c6JnE65V4Zyg==}
+    engines: {node: '>=4'}
+    peerDependencies:
+      '@typescript-eslint/parser': '*'
+      eslint: '*'
+      eslint-import-resolver-node: '*'
+      eslint-import-resolver-typescript: '*'
+      eslint-import-resolver-webpack: '*'
+    peerDependenciesMeta:
+      '@typescript-eslint/parser':
+        optional: true
+      eslint:
+        optional: true
+      eslint-import-resolver-node:
+        optional: true
+      eslint-import-resolver-typescript:
+        optional: true
+      eslint-import-resolver-webpack:
+        optional: true
+
   eslint-plugin-import@2.30.0:
     resolution: {integrity: sha512-/mHNE9jINJfiD2EKkg1BKyPyUk4zdnT54YgbOgfjSakWT5oyX/qQLVNTkehyfpcMxZXMy1zyonZ2v7hZTX43Yw==}
     engines: {node: '>=4'}
@@ -6694,6 +6712,16 @@ packages:
       '@typescript-eslint/parser':
         optional: true
 
+  eslint-plugin-import@2.31.0:
+    resolution: {integrity: sha512-ixmkI62Rbc2/w8Vfxyh1jQRTdRTF52VxwRVHl/ykPAmqG+Nb7/kNn+byLP0LxPgI7zWA16Jt82SybJInmMia3A==}
+    engines: {node: '>=4'}
+    peerDependencies:
+      '@typescript-eslint/parser': '*'
+      eslint: ^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9
+    peerDependenciesMeta:
+      '@typescript-eslint/parser':
+        optional: true
+
   eslint-plugin-vue@9.27.0:
     resolution: {integrity: sha512-5Dw3yxEyuBSXTzT5/Ge1X5kIkRTQ3nvBn/VwPwInNiZBSJOO/timWMUaflONnFBzU6NhB68lxnCda7ULV5N7LA==}
     engines: {node: ^14.17.0 || >=16.0.0}
@@ -9618,10 +9646,6 @@ packages:
   postcss-value-parser@4.2.0:
     resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==}
 
-  postcss@8.4.40:
-    resolution: {integrity: sha512-YF2kKIUzAofPMpfH6hOi2cGnv/HrUlfucspc7pDyvv7kGdqXrfj8SCl/t8owkEgKEuu8ZcRjSOxFxVLqwChZ2Q==}
-    engines: {node: ^10 || ^12 || >=14}
-
   postcss@8.4.47:
     resolution: {integrity: sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==}
     engines: {node: ^10 || ^12 || >=14}
@@ -10174,14 +10198,19 @@ packages:
   safer-buffer@2.1.2:
     resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
 
-  sanitize-html@2.13.0:
-    resolution: {integrity: sha512-Xff91Z+4Mz5QiNSLdLWwjgBDm5b1RU6xBT0+12rapjiaR7SwfRdjw8f+6Rir2MXKLrDicRFHdb51hGOAxmsUIA==}
+  sanitize-html@2.13.1:
+    resolution: {integrity: sha512-ZXtKq89oue4RP7abL9wp/9URJcqQNABB5GGJ2acW1sdO8JTVl92f4ygD7Yc9Ze09VAZhnt2zegeU0tbNsdcLYg==}
 
   sass@1.79.3:
     resolution: {integrity: sha512-m7dZxh0W9EZ3cw50Me5GOuYm/tVAJAn91SUnohLRo9cXBixGUOdvmryN+dXpwR831bhoY3Zv7rEFt85PUwTmzA==}
     engines: {node: '>=14.0.0'}
     hasBin: true
 
+  sass@1.79.4:
+    resolution: {integrity: sha512-K0QDSNPXgyqO4GZq2HO5Q70TLxTH6cIT59RdoCHMivrC8rqzaTw5ab9prjz9KUN1El4FLXrBXJhik61JR4HcGg==}
+    engines: {node: '>=14.0.0'}
+    hasBin: true
+
   sax@1.2.4:
     resolution: {integrity: sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==}
 
@@ -10284,9 +10313,6 @@ packages:
     resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==}
     engines: {node: '>=8'}
 
-  shiki@1.12.0:
-    resolution: {integrity: sha512-BuAxWOm5JhRcbSOl7XCei8wGjgJJonnV0oipUupPY58iULxUGyHhW5CF+9FRMuM1pcJ5cGEJGll1LusX6FwpPA==}
-
   shiki@1.21.0:
     resolution: {integrity: sha512-apCH5BoWTrmHDPGgg3RF8+HAAbEL/CdbYr8rMw7eIrdhCkZHdVGat5mMNlRtd1erNG01VPMIKHNQ0Pj2HMAiog==}
 
@@ -10557,8 +10583,8 @@ packages:
       react-dom:
         optional: true
 
-  storybook@8.3.3:
-    resolution: {integrity: sha512-FG2KAVQN54T9R6voudiEftehtkXtLO+YVGP2gBPfacEdDQjY++ld7kTbHzpTT/bpCDx7Yq3dqOegLm9arVJfYw==}
+  storybook@8.3.4:
+    resolution: {integrity: sha512-nzvuK5TsEgJwcWGLGgafabBOxKn37lfJVv7ZoUVPgJIjk2mNRyJDFwYRJzUZaD37eiR/c/lQ6MoaeqlGwiXoxw==}
     hasBin: true
 
   stream-browserify@3.0.0:
@@ -11445,8 +11471,8 @@ packages:
       typescript:
         optional: true
 
-  vue@3.5.10:
-    resolution: {integrity: sha512-Vy2kmJwHPlouC/tSnIgXVg03SG+9wSqT1xu1Vehc+ChsXsRd7jLkKgMltVEFOzUdBr3uFwBCG+41LJtfAcBRng==}
+  vue@3.5.11:
+    resolution: {integrity: sha512-/8Wurrd9J3lb72FTQS7gRMNQD4nztTtKPmuDuPuhqXmmpD6+skVjAeahNpVzsuky6Sy9gy7wn8UadqPtt9SQIg==}
     peerDependencies:
       typescript: '*'
     peerDependenciesMeta:
@@ -12501,7 +12527,7 @@ snapshots:
   '@babel/template@7.22.15':
     dependencies:
       '@babel/code-frame': 7.24.7
-      '@babel/parser': 7.24.7
+      '@babel/parser': 7.25.6
       '@babel/types': 7.24.7
 
   '@babel/template@7.24.0':
@@ -12513,7 +12539,7 @@ snapshots:
   '@babel/template@7.24.7':
     dependencies:
       '@babel/code-frame': 7.24.7
-      '@babel/parser': 7.24.7
+      '@babel/parser': 7.25.6
       '@babel/types': 7.24.7
 
   '@babel/traverse@7.23.5':
@@ -12524,7 +12550,7 @@ snapshots:
       '@babel/helper-function-name': 7.23.0
       '@babel/helper-hoist-variables': 7.22.5
       '@babel/helper-split-export-declaration': 7.22.6
-      '@babel/parser': 7.24.7
+      '@babel/parser': 7.25.6
       '@babel/types': 7.24.7
       debug: 4.3.7(supports-color@8.1.1)
       globals: 11.12.0
@@ -12539,7 +12565,7 @@ snapshots:
       '@babel/helper-function-name': 7.24.7
       '@babel/helper-hoist-variables': 7.24.7
       '@babel/helper-split-export-declaration': 7.24.7
-      '@babel/parser': 7.24.7
+      '@babel/parser': 7.25.6
       '@babel/types': 7.24.7
       debug: 4.3.7(supports-color@8.1.1)
       globals: 11.12.0
@@ -13652,13 +13678,13 @@ snapshots:
 
   '@misskey-dev/browser-image-resizer@2024.1.0': {}
 
-  '@misskey-dev/eslint-plugin@2.0.3(@eslint/compat@1.1.1)(@typescript-eslint/eslint-plugin@7.17.0(@typescript-eslint/parser@7.17.0(eslint@9.8.0)(typescript@5.6.2))(eslint@9.8.0)(typescript@5.6.2))(@typescript-eslint/parser@7.17.0(eslint@9.8.0)(typescript@5.6.2))(eslint-plugin-import@2.30.0(@typescript-eslint/parser@7.17.0(eslint@9.8.0)(typescript@5.6.2))(eslint@9.8.0))(eslint@9.8.0)(globals@15.9.0)':
+  '@misskey-dev/eslint-plugin@2.0.3(@eslint/compat@1.1.1)(@typescript-eslint/eslint-plugin@7.17.0(@typescript-eslint/parser@7.17.0(eslint@9.8.0)(typescript@5.6.2))(eslint@9.8.0)(typescript@5.6.2))(@typescript-eslint/parser@7.17.0(eslint@9.8.0)(typescript@5.6.2))(eslint-plugin-import@2.31.0(@typescript-eslint/parser@7.17.0(eslint@9.8.0)(typescript@5.6.2))(eslint@9.8.0))(eslint@9.8.0)(globals@15.9.0)':
     dependencies:
       '@eslint/compat': 1.1.1
       '@typescript-eslint/eslint-plugin': 7.17.0(@typescript-eslint/parser@7.17.0(eslint@9.8.0)(typescript@5.6.2))(eslint@9.8.0)(typescript@5.6.2)
       '@typescript-eslint/parser': 7.17.0(eslint@9.8.0)(typescript@5.6.2)
       eslint: 9.8.0
-      eslint-plugin-import: 2.30.0(@typescript-eslint/parser@7.17.0(eslint@9.8.0)(typescript@5.6.2))(eslint@9.8.0)
+      eslint-plugin-import: 2.31.0(@typescript-eslint/parser@7.17.0(eslint@9.8.0)(typescript@5.6.2))(eslint@9.8.0)
       globals: 15.9.0
 
   '@misskey-dev/sharp-read-bmp@1.2.0':
@@ -13834,7 +13860,7 @@ snapshots:
 
   '@npmcli/fs@3.1.0':
     dependencies:
-      semver: 7.6.0
+      semver: 7.6.3
 
   '@nsfw-filter/gif-frames@1.0.2':
     dependencies:
@@ -14344,10 +14370,6 @@ snapshots:
     dependencies:
       '@sentry/types': 8.20.0
 
-  '@shikijs/core@1.12.0':
-    dependencies:
-      '@types/hast': 3.0.4
-
   '@shikijs/core@1.21.0':
     dependencies:
       '@shikijs/engine-javascript': 1.21.0
@@ -14800,120 +14822,120 @@ snapshots:
 
   '@sqltools/formatter@1.2.5': {}
 
-  '@storybook/addon-actions@8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))':
+  '@storybook/addon-actions@8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))':
     dependencies:
       '@storybook/global': 5.0.0
       '@types/uuid': 9.0.8
       dequal: 2.0.3
       polished: 4.2.2
-      storybook: 8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4)
+      storybook: 8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4)
       uuid: 9.0.1
 
-  '@storybook/addon-backgrounds@8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))':
+  '@storybook/addon-backgrounds@8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))':
     dependencies:
       '@storybook/global': 5.0.0
       memoizerific: 1.11.3
-      storybook: 8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4)
+      storybook: 8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4)
       ts-dedent: 2.2.0
 
-  '@storybook/addon-controls@8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))':
+  '@storybook/addon-controls@8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))':
     dependencies:
       '@storybook/global': 5.0.0
       dequal: 2.0.3
       lodash: 4.17.21
-      storybook: 8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4)
+      storybook: 8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4)
       ts-dedent: 2.2.0
 
-  '@storybook/addon-docs@8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))':
+  '@storybook/addon-docs@8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))':
     dependencies:
       '@mdx-js/react': 3.0.1(@types/react@18.0.28)(react@18.3.1)
-      '@storybook/blocks': 8.3.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))
-      '@storybook/csf-plugin': 8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))
+      '@storybook/blocks': 8.3.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))
+      '@storybook/csf-plugin': 8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))
       '@storybook/global': 5.0.0
-      '@storybook/react-dom-shim': 8.3.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))
+      '@storybook/react-dom-shim': 8.3.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))
       '@types/react': 18.0.28
       fs-extra: 11.1.1
       react: 18.3.1
       react-dom: 18.3.1(react@18.3.1)
       rehype-external-links: 3.0.0
       rehype-slug: 6.0.0
-      storybook: 8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4)
+      storybook: 8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4)
       ts-dedent: 2.2.0
 
-  '@storybook/addon-essentials@8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))':
+  '@storybook/addon-essentials@8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))':
     dependencies:
-      '@storybook/addon-actions': 8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))
-      '@storybook/addon-backgrounds': 8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))
-      '@storybook/addon-controls': 8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))
-      '@storybook/addon-docs': 8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))
-      '@storybook/addon-highlight': 8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))
-      '@storybook/addon-measure': 8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))
-      '@storybook/addon-outline': 8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))
-      '@storybook/addon-toolbars': 8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))
-      '@storybook/addon-viewport': 8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))
-      storybook: 8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4)
+      '@storybook/addon-actions': 8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))
+      '@storybook/addon-backgrounds': 8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))
+      '@storybook/addon-controls': 8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))
+      '@storybook/addon-docs': 8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))
+      '@storybook/addon-highlight': 8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))
+      '@storybook/addon-measure': 8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))
+      '@storybook/addon-outline': 8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))
+      '@storybook/addon-toolbars': 8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))
+      '@storybook/addon-viewport': 8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))
+      storybook: 8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4)
       ts-dedent: 2.2.0
 
-  '@storybook/addon-highlight@8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))':
+  '@storybook/addon-highlight@8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))':
     dependencies:
       '@storybook/global': 5.0.0
-      storybook: 8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4)
+      storybook: 8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4)
 
-  '@storybook/addon-interactions@8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))':
+  '@storybook/addon-interactions@8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))':
     dependencies:
       '@storybook/global': 5.0.0
-      '@storybook/instrumenter': 8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))
-      '@storybook/test': 8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))
+      '@storybook/instrumenter': 8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))
+      '@storybook/test': 8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))
       polished: 4.2.2
-      storybook: 8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4)
+      storybook: 8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4)
       ts-dedent: 2.2.0
 
-  '@storybook/addon-links@8.3.3(react@18.3.1)(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))':
+  '@storybook/addon-links@8.3.4(react@18.3.1)(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))':
     dependencies:
       '@storybook/csf': 0.1.11
       '@storybook/global': 5.0.0
-      storybook: 8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4)
+      storybook: 8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4)
       ts-dedent: 2.2.0
     optionalDependencies:
       react: 18.3.1
 
-  '@storybook/addon-mdx-gfm@8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))':
+  '@storybook/addon-mdx-gfm@8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))':
     dependencies:
       remark-gfm: 4.0.0
-      storybook: 8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4)
+      storybook: 8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4)
       ts-dedent: 2.2.0
     transitivePeerDependencies:
       - supports-color
 
-  '@storybook/addon-measure@8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))':
+  '@storybook/addon-measure@8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))':
     dependencies:
       '@storybook/global': 5.0.0
-      storybook: 8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4)
+      storybook: 8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4)
       tiny-invariant: 1.3.3
 
-  '@storybook/addon-outline@8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))':
+  '@storybook/addon-outline@8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))':
     dependencies:
       '@storybook/global': 5.0.0
-      storybook: 8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4)
+      storybook: 8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4)
       ts-dedent: 2.2.0
 
-  '@storybook/addon-storysource@8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))':
+  '@storybook/addon-storysource@8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))':
     dependencies:
-      '@storybook/source-loader': 8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))
+      '@storybook/source-loader': 8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))
       estraverse: 5.3.0
-      storybook: 8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4)
+      storybook: 8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4)
       tiny-invariant: 1.3.3
 
-  '@storybook/addon-toolbars@8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))':
+  '@storybook/addon-toolbars@8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))':
     dependencies:
-      storybook: 8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4)
+      storybook: 8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4)
 
-  '@storybook/addon-viewport@8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))':
+  '@storybook/addon-viewport@8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))':
     dependencies:
       memoizerific: 1.11.3
-      storybook: 8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4)
+      storybook: 8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4)
 
-  '@storybook/blocks@8.3.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))':
+  '@storybook/blocks@8.3.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))':
     dependencies:
       '@storybook/csf': 0.1.11
       '@storybook/global': 5.0.0
@@ -14926,7 +14948,7 @@ snapshots:
       memoizerific: 1.11.3
       polished: 4.2.2
       react-colorful: 5.6.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
-      storybook: 8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4)
+      storybook: 8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4)
       telejson: 7.2.0
       ts-dedent: 2.2.0
       util-deprecate: 1.0.2
@@ -14934,9 +14956,9 @@ snapshots:
       react: 18.3.1
       react-dom: 18.3.1(react@18.3.1)
 
-  '@storybook/builder-vite@8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))(typescript@5.6.2)(vite@5.4.8(@types/node@20.14.12)(sass@1.79.3)(terser@5.33.0))':
+  '@storybook/builder-vite@8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))(typescript@5.6.2)(vite@5.4.8(@types/node@20.14.12)(sass@1.79.3)(terser@5.33.0))':
     dependencies:
-      '@storybook/csf-plugin': 8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))
+      '@storybook/csf-plugin': 8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))
       '@types/find-cache-dir': 3.2.1
       browser-assert: 1.2.1
       es-module-lexer: 1.5.4
@@ -14944,7 +14966,7 @@ snapshots:
       find-cache-dir: 3.3.2
       fs-extra: 11.1.1
       magic-string: 0.30.11
-      storybook: 8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4)
+      storybook: 8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4)
       ts-dedent: 2.2.0
       vite: 5.4.8(@types/node@20.14.12)(sass@1.79.3)(terser@5.33.0)
     optionalDependencies:
@@ -14952,15 +14974,15 @@ snapshots:
     transitivePeerDependencies:
       - supports-color
 
-  '@storybook/components@8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))':
+  '@storybook/components@8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))':
     dependencies:
-      storybook: 8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4)
+      storybook: 8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4)
 
-  '@storybook/core-events@8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))':
+  '@storybook/core-events@8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))':
     dependencies:
-      storybook: 8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4)
+      storybook: 8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4)
 
-  '@storybook/core@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4)':
+  '@storybook/core@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4)':
     dependencies:
       '@storybook/csf': 0.1.11
       '@types/express': 4.17.21
@@ -14980,9 +15002,9 @@ snapshots:
       - supports-color
       - utf-8-validate
 
-  '@storybook/csf-plugin@8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))':
+  '@storybook/csf-plugin@8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))':
     dependencies:
-      storybook: 8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4)
+      storybook: 8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4)
       unplugin: 1.4.0
 
   '@storybook/csf@0.1.11':
@@ -14996,40 +15018,40 @@ snapshots:
       react: 18.3.1
       react-dom: 18.3.1(react@18.3.1)
 
-  '@storybook/instrumenter@8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))':
+  '@storybook/instrumenter@8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))':
     dependencies:
       '@storybook/global': 5.0.0
       '@vitest/utils': 2.1.1
-      storybook: 8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4)
+      storybook: 8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4)
       util: 0.12.5
 
-  '@storybook/manager-api@8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))':
+  '@storybook/manager-api@8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))':
     dependencies:
-      storybook: 8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4)
+      storybook: 8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4)
 
-  '@storybook/preview-api@8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))':
+  '@storybook/preview-api@8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))':
     dependencies:
-      storybook: 8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4)
+      storybook: 8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4)
 
-  '@storybook/react-dom-shim@8.3.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))':
+  '@storybook/react-dom-shim@8.3.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))':
     dependencies:
       react: 18.3.1
       react-dom: 18.3.1(react@18.3.1)
-      storybook: 8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4)
+      storybook: 8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4)
 
-  '@storybook/react-vite@8.3.3(@storybook/test@8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4)))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@4.22.5)(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))(typescript@5.6.2)(vite@5.4.8(@types/node@20.14.12)(sass@1.79.3)(terser@5.33.0))':
+  '@storybook/react-vite@8.3.4(@storybook/test@8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4)))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@4.22.5)(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))(typescript@5.6.2)(vite@5.4.8(@types/node@20.14.12)(sass@1.79.3)(terser@5.33.0))':
     dependencies:
       '@joshwooding/vite-plugin-react-docgen-typescript': 0.3.0(typescript@5.6.2)(vite@5.4.8(@types/node@20.14.12)(sass@1.79.3)(terser@5.33.0))
       '@rollup/pluginutils': 5.1.2(rollup@4.22.5)
-      '@storybook/builder-vite': 8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))(typescript@5.6.2)(vite@5.4.8(@types/node@20.14.12)(sass@1.79.3)(terser@5.33.0))
-      '@storybook/react': 8.3.3(@storybook/test@8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4)))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))(typescript@5.6.2)
+      '@storybook/builder-vite': 8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))(typescript@5.6.2)(vite@5.4.8(@types/node@20.14.12)(sass@1.79.3)(terser@5.33.0))
+      '@storybook/react': 8.3.4(@storybook/test@8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4)))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))(typescript@5.6.2)
       find-up: 5.0.0
       magic-string: 0.30.11
       react: 18.3.1
       react-docgen: 7.0.1
       react-dom: 18.3.1(react@18.3.1)
       resolve: 1.22.8
-      storybook: 8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4)
+      storybook: 8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4)
       tsconfig-paths: 4.2.0
       vite: 5.4.8(@types/node@20.14.12)(sass@1.79.3)(terser@5.33.0)
     transitivePeerDependencies:
@@ -15040,14 +15062,14 @@ snapshots:
       - typescript
       - vite-plugin-glimmerx
 
-  '@storybook/react@8.3.3(@storybook/test@8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4)))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))(typescript@5.6.2)':
+  '@storybook/react@8.3.4(@storybook/test@8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4)))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))(typescript@5.6.2)':
     dependencies:
-      '@storybook/components': 8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))
+      '@storybook/components': 8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))
       '@storybook/global': 5.0.0
-      '@storybook/manager-api': 8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))
-      '@storybook/preview-api': 8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))
-      '@storybook/react-dom-shim': 8.3.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))
-      '@storybook/theming': 8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))
+      '@storybook/manager-api': 8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))
+      '@storybook/preview-api': 8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))
+      '@storybook/react-dom-shim': 8.3.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))
+      '@storybook/theming': 8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))
       '@types/escodegen': 0.0.6
       '@types/estree': 0.0.51
       '@types/node': 22.5.5
@@ -15061,72 +15083,72 @@ snapshots:
       react-dom: 18.3.1(react@18.3.1)
       react-element-to-jsx-string: 15.0.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
       semver: 7.6.3
-      storybook: 8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4)
+      storybook: 8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4)
       ts-dedent: 2.2.0
       type-fest: 2.19.0
       util-deprecate: 1.0.2
     optionalDependencies:
-      '@storybook/test': 8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))
+      '@storybook/test': 8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))
       typescript: 5.6.2
 
-  '@storybook/source-loader@8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))':
+  '@storybook/source-loader@8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))':
     dependencies:
       '@storybook/csf': 0.1.11
       estraverse: 5.3.0
       lodash: 4.17.21
       prettier: 3.3.3
-      storybook: 8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4)
+      storybook: 8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4)
 
-  '@storybook/test@8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))':
+  '@storybook/test@8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))':
     dependencies:
       '@storybook/csf': 0.1.11
       '@storybook/global': 5.0.0
-      '@storybook/instrumenter': 8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))
+      '@storybook/instrumenter': 8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))
       '@testing-library/dom': 10.4.0
       '@testing-library/jest-dom': 6.5.0
       '@testing-library/user-event': 14.5.2(@testing-library/dom@10.4.0)
       '@vitest/expect': 2.0.5
       '@vitest/spy': 2.0.5
-      storybook: 8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4)
+      storybook: 8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4)
       util: 0.12.5
 
-  '@storybook/theming@8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))':
+  '@storybook/theming@8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))':
     dependencies:
-      storybook: 8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4)
+      storybook: 8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4)
 
-  '@storybook/types@8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))':
+  '@storybook/types@8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))':
     dependencies:
-      storybook: 8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4)
+      storybook: 8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4)
 
-  '@storybook/vue3-vite@8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))(vite@5.4.8(@types/node@20.14.12)(sass@1.79.3)(terser@5.33.0))(vue@3.5.10(typescript@5.6.2))':
+  '@storybook/vue3-vite@8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))(vite@5.4.8(@types/node@20.14.12)(sass@1.79.3)(terser@5.33.0))(vue@3.5.11(typescript@5.6.2))':
     dependencies:
-      '@storybook/builder-vite': 8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))(typescript@5.6.2)(vite@5.4.8(@types/node@20.14.12)(sass@1.79.3)(terser@5.33.0))
-      '@storybook/vue3': 8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))(vue@3.5.10(typescript@5.6.2))
+      '@storybook/builder-vite': 8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))(typescript@5.6.2)(vite@5.4.8(@types/node@20.14.12)(sass@1.79.3)(terser@5.33.0))
+      '@storybook/vue3': 8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))(vue@3.5.11(typescript@5.6.2))
       find-package-json: 1.2.0
       magic-string: 0.30.11
-      storybook: 8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4)
+      storybook: 8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4)
       typescript: 5.6.2
       vite: 5.4.8(@types/node@20.14.12)(sass@1.79.3)(terser@5.33.0)
       vue-component-meta: 2.0.16(typescript@5.6.2)
-      vue-docgen-api: 4.75.1(vue@3.5.10(typescript@5.6.2))
+      vue-docgen-api: 4.75.1(vue@3.5.11(typescript@5.6.2))
     transitivePeerDependencies:
       - '@preact/preset-vite'
       - supports-color
       - vite-plugin-glimmerx
       - vue
 
-  '@storybook/vue3@8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))(vue@3.5.10(typescript@5.6.2))':
+  '@storybook/vue3@8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))(vue@3.5.11(typescript@5.6.2))':
     dependencies:
-      '@storybook/components': 8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))
+      '@storybook/components': 8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))
       '@storybook/global': 5.0.0
-      '@storybook/manager-api': 8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))
-      '@storybook/preview-api': 8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))
-      '@storybook/theming': 8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))
-      '@vue/compiler-core': 3.5.7
-      storybook: 8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4)
+      '@storybook/manager-api': 8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))
+      '@storybook/preview-api': 8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))
+      '@storybook/theming': 8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))
+      '@vue/compiler-core': 3.5.10
+      storybook: 8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4)
       ts-dedent: 2.2.0
       type-fest: 2.19.0
-      vue: 3.5.10(typescript@5.6.2)
+      vue: 3.5.11(typescript@5.6.2)
       vue-component-type-helpers: 2.1.6
 
   '@swc/cli@0.3.12(@swc/core@1.6.6)(chokidar@3.5.3)':
@@ -15434,14 +15456,14 @@ snapshots:
     dependencies:
       '@testing-library/dom': 10.4.0
 
-  '@testing-library/vue@8.1.0(@vue/compiler-sfc@3.5.10)(@vue/server-renderer@3.5.10(vue@3.5.10(typescript@5.6.2)))(vue@3.5.10(typescript@5.6.2))':
+  '@testing-library/vue@8.1.0(@vue/compiler-sfc@3.5.11)(@vue/server-renderer@3.5.11(vue@3.5.11(typescript@5.6.2)))(vue@3.5.11(typescript@5.6.2))':
     dependencies:
       '@babel/runtime': 7.23.4
       '@testing-library/dom': 9.3.4
-      '@vue/test-utils': 2.4.1(@vue/server-renderer@3.5.10(vue@3.5.10(typescript@5.6.2)))(vue@3.5.10(typescript@5.6.2))
-      vue: 3.5.10(typescript@5.6.2)
+      '@vue/test-utils': 2.4.1(@vue/server-renderer@3.5.11(vue@3.5.11(typescript@5.6.2)))(vue@3.5.11(typescript@5.6.2))
+      vue: 3.5.11(typescript@5.6.2)
     optionalDependencies:
-      '@vue/compiler-sfc': 3.5.10
+      '@vue/compiler-sfc': 3.5.11
     transitivePeerDependencies:
       - '@vue/server-renderer'
 
@@ -15506,7 +15528,7 @@ snapshots:
       '@types/node': 20.14.12
       '@types/responselike': 1.0.0
 
-  '@types/color-convert@2.0.3':
+  '@types/color-convert@2.0.4':
     dependencies:
       '@types/color-name': 1.1.1
 
@@ -16131,10 +16153,15 @@ snapshots:
 
   '@ungap/structured-clone@1.2.0': {}
 
-  '@vitejs/plugin-vue@5.1.4(vite@5.4.8(@types/node@20.14.12)(sass@1.79.3)(terser@5.33.0))(vue@3.5.10(typescript@5.6.2))':
+  '@vitejs/plugin-vue@5.1.4(vite@5.4.8(@types/node@20.14.12)(sass@1.79.3)(terser@5.33.0))(vue@3.5.11(typescript@5.6.2))':
     dependencies:
       vite: 5.4.8(@types/node@20.14.12)(sass@1.79.3)(terser@5.33.0)
-      vue: 3.5.10(typescript@5.6.2)
+      vue: 3.5.11(typescript@5.6.2)
+
+  '@vitejs/plugin-vue@5.1.4(vite@5.4.8(@types/node@20.14.12)(sass@1.79.4)(terser@5.33.0))(vue@3.5.11(typescript@5.6.2))':
+    dependencies:
+      vite: 5.4.8(@types/node@20.14.12)(sass@1.79.4)(terser@5.33.0)
+      vue: 3.5.11(typescript@5.6.2)
 
   '@vitest/coverage-v8@1.6.0(vitest@1.6.0(@types/node@20.14.12)(happy-dom@10.0.3)(jsdom@24.1.1(bufferutil@4.0.8)(utf-8-validate@6.0.4))(sass@1.79.3)(terser@5.33.0))':
     dependencies:
@@ -16155,7 +16182,7 @@ snapshots:
     transitivePeerDependencies:
       - supports-color
 
-  '@vitest/coverage-v8@1.6.0(vitest@1.6.0(@types/node@20.14.12)(happy-dom@10.0.3)(jsdom@24.1.1)(sass@1.79.3)(terser@5.33.0))':
+  '@vitest/coverage-v8@1.6.0(vitest@1.6.0(@types/node@20.14.12)(happy-dom@10.0.3)(jsdom@24.1.1)(sass@1.79.4)(terser@5.33.0))':
     dependencies:
       '@ampproject/remapping': 2.2.1
       '@bcoe/v8-coverage': 0.2.3
@@ -16170,7 +16197,7 @@ snapshots:
       std-env: 3.7.0
       strip-literal: 2.1.0
       test-exclude: 6.0.0
-      vitest: 1.6.0(@types/node@20.14.12)(happy-dom@10.0.3)(jsdom@24.1.1)(sass@1.79.3)(terser@5.33.0)
+      vitest: 1.6.0(@types/node@20.14.12)(happy-dom@10.0.3)(jsdom@24.1.1)(sass@1.79.4)(terser@5.33.0)
     transitivePeerDependencies:
       - supports-color
 
@@ -16276,10 +16303,10 @@ snapshots:
       estree-walker: 2.0.2
       source-map-js: 1.2.1
 
-  '@vue/compiler-core@3.5.7':
+  '@vue/compiler-core@3.5.11':
     dependencies:
       '@babel/parser': 7.25.6
-      '@vue/shared': 3.5.7
+      '@vue/shared': 3.5.11
       entities: 4.5.0
       estree-walker: 2.0.2
       source-map-js: 1.2.1
@@ -16294,10 +16321,10 @@ snapshots:
       '@vue/compiler-core': 3.5.10
       '@vue/shared': 3.5.10
 
-  '@vue/compiler-dom@3.5.7':
+  '@vue/compiler-dom@3.5.11':
     dependencies:
-      '@vue/compiler-core': 3.5.7
-      '@vue/shared': 3.5.7
+      '@vue/compiler-core': 3.5.11
+      '@vue/shared': 3.5.11
 
   '@vue/compiler-sfc@3.4.37':
     dependencies:
@@ -16311,13 +16338,13 @@ snapshots:
       postcss: 8.4.47
       source-map-js: 1.2.0
 
-  '@vue/compiler-sfc@3.5.10':
+  '@vue/compiler-sfc@3.5.11':
     dependencies:
       '@babel/parser': 7.25.6
-      '@vue/compiler-core': 3.5.10
-      '@vue/compiler-dom': 3.5.10
-      '@vue/compiler-ssr': 3.5.10
-      '@vue/shared': 3.5.10
+      '@vue/compiler-core': 3.5.11
+      '@vue/compiler-dom': 3.5.11
+      '@vue/compiler-ssr': 3.5.11
+      '@vue/shared': 3.5.11
       estree-walker: 2.0.2
       magic-string: 0.30.11
       postcss: 8.4.47
@@ -16328,10 +16355,10 @@ snapshots:
       '@vue/compiler-dom': 3.4.37
       '@vue/shared': 3.4.37
 
-  '@vue/compiler-ssr@3.5.10':
+  '@vue/compiler-ssr@3.5.11':
     dependencies:
-      '@vue/compiler-dom': 3.5.10
-      '@vue/shared': 3.5.10
+      '@vue/compiler-dom': 3.5.11
+      '@vue/shared': 3.5.11
 
   '@vue/compiler-vue2@2.7.16':
     dependencies:
@@ -16341,8 +16368,8 @@ snapshots:
   '@vue/language-core@2.0.16(typescript@5.6.2)':
     dependencies:
       '@volar/language-core': 2.2.0
-      '@vue/compiler-dom': 3.5.7
-      '@vue/shared': 3.5.7
+      '@vue/compiler-dom': 3.5.11
+      '@vue/shared': 3.5.11
       computeds: 0.0.1
       minimatch: 9.0.4
       path-browserify: 1.0.1
@@ -16355,7 +16382,7 @@ snapshots:
       '@volar/language-core': 2.4.5
       '@vue/compiler-dom': 3.4.37
       '@vue/compiler-vue2': 2.7.16
-      '@vue/shared': 3.4.37
+      '@vue/shared': 3.5.11
       computeds: 0.0.1
       minimatch: 9.0.4
       muggle-string: 0.4.1
@@ -16367,19 +16394,19 @@ snapshots:
     dependencies:
       '@vue/shared': 3.4.37
 
-  '@vue/reactivity@3.5.10':
+  '@vue/reactivity@3.5.11':
     dependencies:
-      '@vue/shared': 3.5.10
+      '@vue/shared': 3.5.11
 
   '@vue/runtime-core@3.4.37':
     dependencies:
       '@vue/reactivity': 3.4.37
       '@vue/shared': 3.4.37
 
-  '@vue/runtime-core@3.5.10':
+  '@vue/runtime-core@3.5.11':
     dependencies:
-      '@vue/reactivity': 3.5.10
-      '@vue/shared': 3.5.10
+      '@vue/reactivity': 3.5.11
+      '@vue/shared': 3.5.11
 
   '@vue/runtime-dom@3.4.37':
     dependencies:
@@ -16388,11 +16415,11 @@ snapshots:
       '@vue/shared': 3.4.37
       csstype: 3.1.3
 
-  '@vue/runtime-dom@3.5.10':
+  '@vue/runtime-dom@3.5.11':
     dependencies:
-      '@vue/reactivity': 3.5.10
-      '@vue/runtime-core': 3.5.10
-      '@vue/shared': 3.5.10
+      '@vue/reactivity': 3.5.11
+      '@vue/runtime-core': 3.5.11
+      '@vue/shared': 3.5.11
       csstype: 3.1.3
 
   '@vue/server-renderer@3.4.37(vue@3.4.37(typescript@5.5.4))':
@@ -16401,25 +16428,25 @@ snapshots:
       '@vue/shared': 3.4.37
       vue: 3.4.37(typescript@5.5.4)
 
-  '@vue/server-renderer@3.5.10(vue@3.5.10(typescript@5.6.2))':
+  '@vue/server-renderer@3.5.11(vue@3.5.11(typescript@5.6.2))':
     dependencies:
-      '@vue/compiler-ssr': 3.5.10
-      '@vue/shared': 3.5.10
-      vue: 3.5.10(typescript@5.6.2)
+      '@vue/compiler-ssr': 3.5.11
+      '@vue/shared': 3.5.11
+      vue: 3.5.11(typescript@5.6.2)
 
   '@vue/shared@3.4.37': {}
 
   '@vue/shared@3.5.10': {}
 
-  '@vue/shared@3.5.7': {}
+  '@vue/shared@3.5.11': {}
 
-  '@vue/test-utils@2.4.1(@vue/server-renderer@3.5.10(vue@3.5.10(typescript@5.6.2)))(vue@3.5.10(typescript@5.6.2))':
+  '@vue/test-utils@2.4.1(@vue/server-renderer@3.5.11(vue@3.5.11(typescript@5.6.2)))(vue@3.5.11(typescript@5.6.2))':
     dependencies:
       js-beautify: 1.14.9
-      vue: 3.5.10(typescript@5.6.2)
+      vue: 3.5.11(typescript@5.6.2)
       vue-component-type-helpers: 1.8.4
     optionalDependencies:
-      '@vue/server-renderer': 3.5.10(vue@3.5.10(typescript@5.6.2))
+      '@vue/server-renderer': 3.5.11(vue@3.5.11(typescript@5.6.2))
 
   '@webgpu/types@0.1.30': {}
 
@@ -16973,14 +17000,14 @@ snapshots:
       node-gyp-build: 4.6.0
     optional: true
 
-  bullmq@5.13.2:
+  bullmq@5.15.0:
     dependencies:
       cron-parser: 4.8.1
       ioredis: 5.4.1
       msgpackr: 1.10.1
       node-abort-controller: 3.1.1
-      semver: 7.6.0
-      tslib: 2.6.3
+      semver: 7.6.3
+      tslib: 2.7.0
       uuid: 9.0.1
     transitivePeerDependencies:
       - supports-color
@@ -17228,7 +17255,7 @@ snapshots:
 
   chownr@2.0.0: {}
 
-  chromatic@11.10.4: {}
+  chromatic@11.11.0: {}
 
   ci-info@3.7.1: {}
 
@@ -17488,12 +17515,12 @@ snapshots:
   css-tree@2.2.1:
     dependencies:
       mdn-data: 2.0.28
-      source-map-js: 1.2.0
+      source-map-js: 1.2.1
 
   css-tree@2.3.1:
     dependencies:
       mdn-data: 2.0.30
-      source-map-js: 1.2.0
+      source-map-js: 1.2.1
 
   css-what@6.1.0: {}
 
@@ -17927,7 +17954,7 @@ snapshots:
       '@one-ini/wasm': 0.1.1
       commander: 10.0.1
       minimatch: 9.0.1
-      semver: 7.6.0
+      semver: 7.6.3
 
   ee-first@1.1.1: {}
 
@@ -18321,7 +18348,17 @@ snapshots:
     transitivePeerDependencies:
       - supports-color
 
-  eslint-module-utils@2.11.0(@typescript-eslint/parser@7.17.0(eslint@9.8.0)(typescript@5.6.2))(eslint-import-resolver-node@0.3.9)(eslint@9.8.0):
+  eslint-module-utils@2.12.0(@typescript-eslint/parser@7.17.0(eslint@9.11.0)(typescript@5.6.2))(eslint-import-resolver-node@0.3.9)(eslint@9.11.0):
+    dependencies:
+      debug: 3.2.7(supports-color@8.1.1)
+    optionalDependencies:
+      '@typescript-eslint/parser': 7.17.0(eslint@9.11.0)(typescript@5.6.2)
+      eslint: 9.11.0
+      eslint-import-resolver-node: 0.3.9
+    transitivePeerDependencies:
+      - supports-color
+
+  eslint-module-utils@2.12.0(@typescript-eslint/parser@7.17.0(eslint@9.8.0)(typescript@5.6.2))(eslint-import-resolver-node@0.3.9)(eslint@9.8.0):
     dependencies:
       debug: 3.2.7(supports-color@8.1.1)
     optionalDependencies:
@@ -18359,7 +18396,36 @@ snapshots:
       - eslint-import-resolver-webpack
       - supports-color
 
-  eslint-plugin-import@2.30.0(@typescript-eslint/parser@7.17.0(eslint@9.8.0)(typescript@5.6.2))(eslint@9.8.0):
+  eslint-plugin-import@2.31.0(@typescript-eslint/parser@7.17.0(eslint@9.11.0)(typescript@5.6.2))(eslint@9.11.0):
+    dependencies:
+      '@rtsao/scc': 1.1.0
+      array-includes: 3.1.8
+      array.prototype.findlastindex: 1.2.5
+      array.prototype.flat: 1.3.2
+      array.prototype.flatmap: 1.3.2
+      debug: 3.2.7(supports-color@8.1.1)
+      doctrine: 2.1.0
+      eslint: 9.11.0
+      eslint-import-resolver-node: 0.3.9
+      eslint-module-utils: 2.12.0(@typescript-eslint/parser@7.17.0(eslint@9.11.0)(typescript@5.6.2))(eslint-import-resolver-node@0.3.9)(eslint@9.11.0)
+      hasown: 2.0.2
+      is-core-module: 2.15.1
+      is-glob: 4.0.3
+      minimatch: 3.1.2
+      object.fromentries: 2.0.8
+      object.groupby: 1.0.3
+      object.values: 1.2.0
+      semver: 6.3.1
+      string.prototype.trimend: 1.0.8
+      tsconfig-paths: 3.15.0
+    optionalDependencies:
+      '@typescript-eslint/parser': 7.17.0(eslint@9.11.0)(typescript@5.6.2)
+    transitivePeerDependencies:
+      - eslint-import-resolver-typescript
+      - eslint-import-resolver-webpack
+      - supports-color
+
+  eslint-plugin-import@2.31.0(@typescript-eslint/parser@7.17.0(eslint@9.8.0)(typescript@5.6.2))(eslint@9.8.0):
     dependencies:
       '@rtsao/scc': 1.1.0
       array-includes: 3.1.8
@@ -18370,7 +18436,7 @@ snapshots:
       doctrine: 2.1.0
       eslint: 9.8.0
       eslint-import-resolver-node: 0.3.9
-      eslint-module-utils: 2.11.0(@typescript-eslint/parser@7.17.0(eslint@9.8.0)(typescript@5.6.2))(eslint-import-resolver-node@0.3.9)(eslint@9.8.0)
+      eslint-module-utils: 2.12.0(@typescript-eslint/parser@7.17.0(eslint@9.8.0)(typescript@5.6.2))(eslint-import-resolver-node@0.3.9)(eslint@9.8.0)
       hasown: 2.0.2
       is-core-module: 2.15.1
       is-glob: 4.0.3
@@ -18379,6 +18445,7 @@ snapshots:
       object.groupby: 1.0.3
       object.values: 1.2.0
       semver: 6.3.1
+      string.prototype.trimend: 1.0.8
       tsconfig-paths: 3.15.0
     optionalDependencies:
       '@typescript-eslint/parser': 7.17.0(eslint@9.8.0)(typescript@5.6.2)
@@ -19633,7 +19700,7 @@ snapshots:
 
   is-boolean-object@1.1.2:
     dependencies:
-      call-bind: 1.0.2
+      call-bind: 1.0.7
       has-tostringtag: 1.0.0
 
   is-buffer@1.1.6: {}
@@ -19781,7 +19848,7 @@ snapshots:
 
   is-weakset@2.0.2:
     dependencies:
-      call-bind: 1.0.2
+      call-bind: 1.0.7
       get-intrinsic: 1.2.1
 
   is-wsl@2.2.0:
@@ -19818,7 +19885,7 @@ snapshots:
       '@babel/parser': 7.24.7
       '@istanbuljs/schema': 0.1.3
       istanbul-lib-coverage: 3.2.2
-      semver: 7.6.0
+      semver: 7.6.3
     transitivePeerDependencies:
       - supports-color
 
@@ -20545,7 +20612,7 @@ snapshots:
 
   lru-cache@10.0.2:
     dependencies:
-      semver: 7.6.0
+      semver: 7.6.3
 
   lru-cache@10.2.2: {}
 
@@ -20596,7 +20663,7 @@ snapshots:
 
   make-dir@4.0.0:
     dependencies:
-      semver: 7.6.0
+      semver: 7.6.3
 
   make-fetch-happen@13.0.0:
     dependencies:
@@ -21395,7 +21462,7 @@ snapshots:
     dependencies:
       hosted-git-info: 4.1.0
       is-core-module: 2.13.1
-      semver: 7.6.0
+      semver: 7.6.3
       validate-npm-package-license: 3.0.4
 
   normalize-path@3.0.0: {}
@@ -22006,12 +22073,6 @@ snapshots:
 
   postcss-value-parser@4.2.0: {}
 
-  postcss@8.4.40:
-    dependencies:
-      nanoid: 3.3.7
-      picocolors: 1.0.1
-      source-map-js: 1.2.0
-
   postcss@8.4.47:
     dependencies:
       nanoid: 3.3.7
@@ -22664,14 +22725,14 @@ snapshots:
 
   safer-buffer@2.1.2: {}
 
-  sanitize-html@2.13.0:
+  sanitize-html@2.13.1:
     dependencies:
       deepmerge: 4.2.2
       escape-string-regexp: 4.0.0
       htmlparser2: 8.0.1
       is-plain-object: 5.0.0
       parse-srcset: 1.0.2
-      postcss: 8.4.40
+      postcss: 8.4.47
 
   sass@1.79.3:
     dependencies:
@@ -22679,6 +22740,12 @@ snapshots:
       immutable: 4.2.2
       source-map-js: 1.2.0
 
+  sass@1.79.4:
+    dependencies:
+      chokidar: 3.5.3
+      immutable: 4.2.2
+      source-map-js: 1.2.1
+
   sax@1.2.4: {}
 
   saxes@6.0.0:
@@ -22809,11 +22876,6 @@ snapshots:
 
   shebang-regex@3.0.0: {}
 
-  shiki@1.12.0:
-    dependencies:
-      '@shikijs/core': 1.12.0
-      '@types/hast': 3.0.4
-
   shiki@1.21.0:
     dependencies:
       '@shikijs/core': 1.21.0
@@ -23073,22 +23135,22 @@ snapshots:
     dependencies:
       internal-slot: 1.0.5
 
-  storybook-addon-misskey-theme@https://codeload.github.com/misskey-dev/storybook-addon-misskey-theme/tar.gz/cf583db098365b2ccc81a82f63ca9c93bc32b640(@storybook/blocks@8.3.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4)))(@storybook/components@8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4)))(@storybook/core-events@8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4)))(@storybook/manager-api@8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4)))(@storybook/preview-api@8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4)))(@storybook/theming@8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4)))(@storybook/types@8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4)))(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
+  storybook-addon-misskey-theme@https://codeload.github.com/misskey-dev/storybook-addon-misskey-theme/tar.gz/cf583db098365b2ccc81a82f63ca9c93bc32b640(@storybook/blocks@8.3.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4)))(@storybook/components@8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4)))(@storybook/core-events@8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4)))(@storybook/manager-api@8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4)))(@storybook/preview-api@8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4)))(@storybook/theming@8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4)))(@storybook/types@8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4)))(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
     dependencies:
-      '@storybook/blocks': 8.3.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))
-      '@storybook/components': 8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))
-      '@storybook/core-events': 8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))
-      '@storybook/manager-api': 8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))
-      '@storybook/preview-api': 8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))
-      '@storybook/theming': 8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))
-      '@storybook/types': 8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))
+      '@storybook/blocks': 8.3.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))
+      '@storybook/components': 8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))
+      '@storybook/core-events': 8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))
+      '@storybook/manager-api': 8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))
+      '@storybook/preview-api': 8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))
+      '@storybook/theming': 8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))
+      '@storybook/types': 8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))
     optionalDependencies:
       react: 18.3.1
       react-dom: 18.3.1(react@18.3.1)
 
-  storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4):
+  storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4):
     dependencies:
-      '@storybook/core': 8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4)
+      '@storybook/core': 8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4)
     transitivePeerDependencies:
       - bufferutil
       - supports-color
@@ -23765,13 +23827,13 @@ snapshots:
 
   uuid@9.0.1: {}
 
-  v-code-diff@1.13.1(vue@3.5.10(typescript@5.6.2)):
+  v-code-diff@1.13.1(vue@3.5.11(typescript@5.6.2)):
     dependencies:
       diff: 5.2.0
       diff-match-patch: 1.0.5
       highlight.js: 11.10.0
-      vue: 3.5.10(typescript@5.6.2)
-      vue-demi: 0.14.7(vue@3.5.10(typescript@5.6.2))
+      vue: 3.5.11(typescript@5.6.2)
+      vue-demi: 0.14.7(vue@3.5.11(typescript@5.6.2))
 
   v8-to-istanbul@9.2.0:
     dependencies:
@@ -23823,6 +23885,24 @@ snapshots:
       - supports-color
       - terser
 
+  vite-node@1.6.0(@types/node@20.14.12)(sass@1.79.4)(terser@5.33.0):
+    dependencies:
+      cac: 6.7.14
+      debug: 4.3.5(supports-color@8.1.1)
+      pathe: 1.1.2
+      picocolors: 1.0.1
+      vite: 5.4.8(@types/node@20.14.12)(sass@1.79.4)(terser@5.33.0)
+    transitivePeerDependencies:
+      - '@types/node'
+      - less
+      - lightningcss
+      - sass
+      - sass-embedded
+      - stylus
+      - sugarss
+      - supports-color
+      - terser
+
   vite-plugin-turbosnap@1.0.3: {}
 
   vite@5.4.8(@types/node@20.14.12)(sass@1.79.3)(terser@5.33.0):
@@ -23836,6 +23916,17 @@ snapshots:
       sass: 1.79.3
       terser: 5.33.0
 
+  vite@5.4.8(@types/node@20.14.12)(sass@1.79.4)(terser@5.33.0):
+    dependencies:
+      esbuild: 0.21.5
+      postcss: 8.4.47
+      rollup: 4.22.5
+    optionalDependencies:
+      '@types/node': 20.14.12
+      fsevents: 2.3.3
+      sass: 1.79.4
+      terser: 5.33.0
+
   vitest-fetch-mock@0.2.2(encoding@0.1.13)(vitest@1.6.0(@types/node@20.14.12)(happy-dom@10.0.3)(jsdom@24.1.1(bufferutil@4.0.8)(utf-8-validate@6.0.4))(sass@1.79.3)(terser@5.33.0)):
     dependencies:
       cross-fetch: 3.1.6(encoding@0.1.13)
@@ -23879,7 +23970,7 @@ snapshots:
       - supports-color
       - terser
 
-  vitest@1.6.0(@types/node@20.14.12)(happy-dom@10.0.3)(jsdom@24.1.1)(sass@1.79.3)(terser@5.33.0):
+  vitest@1.6.0(@types/node@20.14.12)(happy-dom@10.0.3)(jsdom@24.1.1)(sass@1.79.4)(terser@5.33.0):
     dependencies:
       '@vitest/expect': 1.6.0
       '@vitest/runner': 1.6.0
@@ -23898,8 +23989,8 @@ snapshots:
       strip-literal: 2.1.0
       tinybench: 2.6.0
       tinypool: 0.8.4
-      vite: 5.4.8(@types/node@20.14.12)(sass@1.79.3)(terser@5.33.0)
-      vite-node: 1.6.0(@types/node@20.14.12)(sass@1.79.3)(terser@5.33.0)
+      vite: 5.4.8(@types/node@20.14.12)(sass@1.79.4)(terser@5.33.0)
+      vite-node: 1.6.0(@types/node@20.14.12)(sass@1.79.4)(terser@5.33.0)
       why-is-node-running: 2.2.2
     optionalDependencies:
       '@types/node': 20.14.12
@@ -23922,7 +24013,7 @@ snapshots:
   vscode-languageclient@9.0.1:
     dependencies:
       minimatch: 5.1.2
-      semver: 7.6.0
+      semver: 7.6.3
       vscode-languageserver-protocol: 3.17.5
 
   vscode-languageserver-protocol@3.17.5:
@@ -23955,24 +24046,24 @@ snapshots:
 
   vue-component-type-helpers@2.1.6: {}
 
-  vue-demi@0.14.7(vue@3.5.10(typescript@5.6.2)):
+  vue-demi@0.14.7(vue@3.5.11(typescript@5.6.2)):
     dependencies:
-      vue: 3.5.10(typescript@5.6.2)
+      vue: 3.5.11(typescript@5.6.2)
 
-  vue-docgen-api@4.75.1(vue@3.5.10(typescript@5.6.2)):
+  vue-docgen-api@4.75.1(vue@3.5.11(typescript@5.6.2)):
     dependencies:
       '@babel/parser': 7.25.6
       '@babel/types': 7.25.6
-      '@vue/compiler-dom': 3.5.7
-      '@vue/compiler-sfc': 3.5.10
+      '@vue/compiler-dom': 3.5.10
+      '@vue/compiler-sfc': 3.5.11
       ast-types: 0.16.1
       hash-sum: 2.0.0
       lru-cache: 8.0.4
       pug: 3.0.3
       recast: 0.23.6
       ts-map: 1.0.3
-      vue: 3.5.10(typescript@5.6.2)
-      vue-inbrowser-compiler-independent-utils: 4.71.1(vue@3.5.10(typescript@5.6.2))
+      vue: 3.5.11(typescript@5.6.2)
+      vue-inbrowser-compiler-independent-utils: 4.71.1(vue@3.5.11(typescript@5.6.2))
 
   vue-eslint-parser@9.4.3(eslint@9.11.0):
     dependencies:
@@ -23987,9 +24078,9 @@ snapshots:
     transitivePeerDependencies:
       - supports-color
 
-  vue-inbrowser-compiler-independent-utils@4.71.1(vue@3.5.10(typescript@5.6.2)):
+  vue-inbrowser-compiler-independent-utils@4.71.1(vue@3.5.11(typescript@5.6.2)):
     dependencies:
-      vue: 3.5.10(typescript@5.6.2)
+      vue: 3.5.11(typescript@5.6.2)
 
   vue-template-compiler@2.7.14:
     dependencies:
@@ -24013,20 +24104,20 @@ snapshots:
     optionalDependencies:
       typescript: 5.5.4
 
-  vue@3.5.10(typescript@5.6.2):
+  vue@3.5.11(typescript@5.6.2):
     dependencies:
-      '@vue/compiler-dom': 3.5.10
-      '@vue/compiler-sfc': 3.5.10
-      '@vue/runtime-dom': 3.5.10
-      '@vue/server-renderer': 3.5.10(vue@3.5.10(typescript@5.6.2))
-      '@vue/shared': 3.5.10
+      '@vue/compiler-dom': 3.5.11
+      '@vue/compiler-sfc': 3.5.11
+      '@vue/runtime-dom': 3.5.11
+      '@vue/server-renderer': 3.5.11(vue@3.5.11(typescript@5.6.2))
+      '@vue/shared': 3.5.11
     optionalDependencies:
       typescript: 5.6.2
 
-  vuedraggable@4.1.0(vue@3.5.10(typescript@5.6.2)):
+  vuedraggable@4.1.0(vue@3.5.11(typescript@5.6.2)):
     dependencies:
       sortablejs: 1.14.0
-      vue: 3.5.10(typescript@5.6.2)
+      vue: 3.5.11(typescript@5.6.2)
 
   w3c-xmlserializer@5.0.0:
     dependencies:

From ed71b0b7d44ea5c56d1afa5dd113330832e60155 Mon Sep 17 00:00:00 2001
From: syuilo <4439005+syuilo@users.noreply.github.com>
Date: Fri, 4 Oct 2024 11:27:08 +0900
Subject: [PATCH 028/121] :art:

---
 packages/frontend/src/components/MkAbuseReport.vue | 1 +
 1 file changed, 1 insertion(+)

diff --git a/packages/frontend/src/components/MkAbuseReport.vue b/packages/frontend/src/components/MkAbuseReport.vue
index aa2bffaa17..c9c629046e 100644
--- a/packages/frontend/src/components/MkAbuseReport.vue
+++ b/packages/frontend/src/components/MkAbuseReport.vue
@@ -10,6 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 		<i v-else class="ti ti-exclamation-circle" style="color: var(--warn)"></i>
 	</template>
 	<template #label><MkAcct :user="report.targetUser"/> (by <MkAcct :user="report.reporter"/>)</template>
+	<template #caption>{{ report.comment }}</template>
 	<template #suffix><MkTime :time="report.createdAt"/></template>
 	<template v-if="!report.resolved" #footer>
 		<div class="_buttons">

From 2fa805b8f79928f00313c0df82cf529c6244b764 Mon Sep 17 00:00:00 2001
From: syuilo <4439005+syuilo@users.noreply.github.com>
Date: Fri, 4 Oct 2024 11:55:46 +0900
Subject: [PATCH 029/121] :art:

---
 packages/frontend/src/pages/admin/system-webhook.item.vue | 1 +
 1 file changed, 1 insertion(+)

diff --git a/packages/frontend/src/pages/admin/system-webhook.item.vue b/packages/frontend/src/pages/admin/system-webhook.item.vue
index 4e767fba16..124790338c 100644
--- a/packages/frontend/src/pages/admin/system-webhook.item.vue
+++ b/packages/frontend/src/pages/admin/system-webhook.item.vue
@@ -6,6 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 <template>
 <MkFolder>
 	<template #label>{{ entity.name || entity.url }}</template>
+	<template v-if="entity.name != null && entity.name != ''" #caption>{{ entity.url }}</template>
 	<template #icon>
 		<i v-if="!entity.isActive" class="ti ti-player-pause"/>
 		<i v-else-if="entity.latestStatus === null" class="ti ti-circle"/>

From 1aee26039855b087fe7e65d756d422b100b415db Mon Sep 17 00:00:00 2001
From: syuilo <4439005+syuilo@users.noreply.github.com>
Date: Fri, 4 Oct 2024 12:23:24 +0900
Subject: [PATCH 030/121] fix test

---
 .../src/components => idea}/MkAbuseReport.stories.impl.ts     | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)
 rename {packages/frontend/src/components => idea}/MkAbuseReport.stories.impl.ts (88%)

diff --git a/packages/frontend/src/components/MkAbuseReport.stories.impl.ts b/idea/MkAbuseReport.stories.impl.ts
similarity index 88%
rename from packages/frontend/src/components/MkAbuseReport.stories.impl.ts
rename to idea/MkAbuseReport.stories.impl.ts
index cf09c96fd4..717bceb23d 100644
--- a/packages/frontend/src/components/MkAbuseReport.stories.impl.ts
+++ b/idea/MkAbuseReport.stories.impl.ts
@@ -7,8 +7,8 @@
 import { action } from '@storybook/addon-actions';
 import { StoryObj } from '@storybook/vue3';
 import { HttpResponse, http } from 'msw';
-import { abuseUserReport } from '../../.storybook/fakes.js';
-import { commonHandlers } from '../../.storybook/mocks.js';
+import { abuseUserReport } from '../packages/frontend/.storybook/fakes.js';
+import { commonHandlers } from '../packages/frontend/.storybook/mocks.js';
 import MkAbuseReport from './MkAbuseReport.vue';
 export const Default = {
 	render(args) {

From e34465027887e07fbcd6acdd512a124cfcefe032 Mon Sep 17 00:00:00 2001
From: syuilo <4439005+syuilo@users.noreply.github.com>
Date: Fri, 4 Oct 2024 13:40:49 +0900
Subject: [PATCH 031/121] Update generate.tsx

---
 packages/frontend/.storybook/generate.tsx | 13 ++++++++++++-
 1 file changed, 12 insertions(+), 1 deletion(-)

diff --git a/packages/frontend/.storybook/generate.tsx b/packages/frontend/.storybook/generate.tsx
index 42d1a10f0a..f2bdc631d2 100644
--- a/packages/frontend/.storybook/generate.tsx
+++ b/packages/frontend/.storybook/generate.tsx
@@ -397,7 +397,18 @@ function toStories(component: string): Promise<string> {
 	const globs = await Promise.all([
 		glob('src/components/global/Mk*.vue'),
 		glob('src/components/global/RouterView.vue'),
-		glob('src/components/Mk[A-E]*.vue'),
+		glob('src/components/MkAbuseReportWindow.vue'),
+		glob('src/components/MkAccountMoved.vue'),
+		glob('src/components/MkAchievements.vue'),
+		glob('src/components/MkAnalogClock.vue'),
+		glob('src/components/MkAnimBg.vue'),
+		glob('src/components/MkAnnouncementDialog.vue'),
+		glob('src/components/MkAntennaEditor.vue'),
+		glob('src/components/MkAntennaEditorDialog.vue'),
+		glob('src/components/MkAsUi.vue'),
+		glob('src/components/MkAutocomplete.vue'),
+		glob('src/components/MkAvatars.vue'),
+		glob('src/components/Mk[B-E]*.vue'),
 		glob('src/components/MkFlashPreview.vue'),
 		glob('src/components/MkGalleryPostPreview.vue'),
 		glob('src/components/MkSignupServerRules.vue'),

From 975c2e7bc567618c3f8b0082afcba6530d679dae Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=8B=E3=81=A3=E3=81=93=E3=81=8B=E3=82=8A?=
 <67428053+kakkokari-gtyih@users.noreply.github.com>
Date: Fri, 4 Oct 2024 15:23:33 +0900
Subject: [PATCH 032/121] =?UTF-8?q?enhance(frontend):=20=E3=82=B5=E3=82=A4?=
 =?UTF-8?q?=E3=83=B3=E3=82=A4=E3=83=B3=E7=94=BB=E9=9D=A2=E3=81=AE=E6=94=B9?=
 =?UTF-8?q?=E5=96=84=20(#14658)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

* wip

* Update MkSignin.vue

* Update MkSignin.vue

* wip

* Update CHANGELOG.md

* enhance(frontend): サインイン画面の改善

* Update Changelog

* 14655の変更取り込み

* spdx

* fix

* fix

* fix

* :art:

* :art:

* :art:

* :art:

* Captchaがリセットされない問題を修正

* 次の処理をsignin apiから読み取るように

* Add Comments

* fix

* fix test

* attempt to fix test

* fix test

* fix test

* fix test

* fix

* fix test

* fix: 一部のエラーがちゃんと出るように

* Update Changelog

* :art:

* :art:

* remove border

---------

Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com>
---
 CHANGELOG.md                                  |   4 +
 cypress/e2e/basic.cy.ts                       |  14 +-
 cypress/support/commands.ts                   |   4 +-
 locales/index.d.ts                            |   4 +
 locales/ja-JP.yml                             |   1 +
 .../src/core/entities/UserEntityService.ts    |  13 +-
 .../backend/src/models/json-schema/user.ts    |  42 +-
 .../src/server/api/SigninApiService.ts        |  86 ++-
 packages/backend/test/e2e/2fa.ts              |  99 +--
 packages/backend/test/e2e/users.ts            |  15 +-
 .../src/components/MkSignin.input.vue         | 206 +++++
 .../src/components/MkSignin.passkey.vue       |  92 +++
 .../src/components/MkSignin.password.vue      | 181 +++++
 .../frontend/src/components/MkSignin.totp.vue |  74 ++
 packages/frontend/src/components/MkSignin.vue | 716 +++++++++---------
 .../src/components/MkSigninDialog.vue         |  80 +-
 packages/misskey-js/etc/misskey-js.api.md     |   2 +-
 packages/misskey-js/src/autogen/types.ts      |  15 +-
 packages/misskey-js/src/entities.ts           |   2 +-
 19 files changed, 1161 insertions(+), 489 deletions(-)
 create mode 100644 packages/frontend/src/components/MkSignin.input.vue
 create mode 100644 packages/frontend/src/components/MkSignin.passkey.vue
 create mode 100644 packages/frontend/src/components/MkSignin.password.vue
 create mode 100644 packages/frontend/src/components/MkSignin.totp.vue

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9e4ddabd55..a31be063f0 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,8 @@
 - サーバー初期設定時に使用する初期パスワードを設定できるようになりました。今後Misskeyサーバーを新たに設置する際には、初回の起動前にコンフィグファイルの`setupPassword`をコメントアウトし、初期パスワードを設定することをおすすめします。(すでに初期設定を完了しているサーバーについては、この変更に伴い対応する必要はありません)  
   - ホスティングサービスを運営している場合は、コンフィグファイルを構築する際に`setupPassword`をランダムな値に設定し、ユーザーに通知するようにシステムを更新することをおすすめします。
   - なお、初期パスワードが設定されていない場合でも初期設定を行うことが可能です(UI上で初期パスワードの入力欄を空欄にすると続行できます)。
+- ユーザーデータを読み込む際の型が一部変更されました。
+	- `twoFactorEnabled`, `usePasswordLessLogin`, `securityKeys`: 自分とモデレーター以外のユーザーからは取得できなくなりました
 
 ### General
 - Feat: サーバー初期設定時に初期パスワードを設定できるように
@@ -14,9 +16,11 @@
 
 ### Client
 - Enhance: デザインの調整
+- Enhance: ログイン画面の認証フローを改善
 
 ### Server
 - Enhance: セキュリティ向上のため、ログイン時にメール通知を行うように
+- Enhance: 自分とモデレーター以外のユーザーから二要素認証関連のデータが取得できないように
 
 
 ## 2024.9.0
diff --git a/cypress/e2e/basic.cy.ts b/cypress/e2e/basic.cy.ts
index e4baeacbf3..c9d7e0a24a 100644
--- a/cypress/e2e/basic.cy.ts
+++ b/cypress/e2e/basic.cy.ts
@@ -123,8 +123,13 @@ describe('After user signup', () => {
 		cy.intercept('POST', '/api/signin').as('signin');
 
 		cy.get('[data-cy-signin]').click();
-		cy.get('[data-cy-signin-username] input').type('alice');
-		// Enterキーでサインインできるかの確認も兼ねる
+
+		cy.get('[data-cy-signin-page-input]').should('be.visible', { timeout: 1000 });
+		// Enterキーで続行できるかの確認も兼ねる
+		cy.get('[data-cy-signin-username] input').type('alice{enter}');
+
+		cy.get('[data-cy-signin-page-password]').should('be.visible', { timeout: 10000 });
+		// Enterキーで続行できるかの確認も兼ねる
 		cy.get('[data-cy-signin-password] input').type('alice1234{enter}');
 
 		cy.wait('@signin');
@@ -139,8 +144,9 @@ describe('After user signup', () => {
 		cy.visitHome();
 
 		cy.get('[data-cy-signin]').click();
-		cy.get('[data-cy-signin-username] input').type('alice');
-		cy.get('[data-cy-signin-password] input').type('alice1234{enter}');
+
+		cy.get('[data-cy-signin-page-input]').should('be.visible', { timeout: 1000 });
+		cy.get('[data-cy-signin-username] input').type('alice{enter}');
 
 		// TODO: cypressにブラウザの言語指定できる機能が実装され次第英語のみテストするようにする
 		cy.contains(/アカウントが凍結されています|This account has been suspended due to/gi);
diff --git a/cypress/support/commands.ts b/cypress/support/commands.ts
index 3cdf4e2087..ed5cda31b0 100644
--- a/cypress/support/commands.ts
+++ b/cypress/support/commands.ts
@@ -58,7 +58,9 @@ Cypress.Commands.add('login', (username, password) => {
 	cy.intercept('POST', '/api/signin').as('signin');
 
 	cy.get('[data-cy-signin]').click();
-	cy.get('[data-cy-signin-username] input').type(username);
+	cy.get('[data-cy-signin-page-input]').should('be.visible', { timeout: 1000 });
+	cy.get('[data-cy-signin-username] input').type(`${username}{enter}`);
+	cy.get('[data-cy-signin-page-password]').should('be.visible', { timeout: 10000 });
 	cy.get('[data-cy-signin-password] input').type(`${password}{enter}`);
 
 	cy.wait('@signin').as('signedIn');
diff --git a/locales/index.d.ts b/locales/index.d.ts
index 86a6df3100..1a0547ebc6 100644
--- a/locales/index.d.ts
+++ b/locales/index.d.ts
@@ -3714,6 +3714,10 @@ export interface Locale extends ILocale {
      * パスワードが間違っています。
      */
     "incorrectPassword": string;
+    /**
+     * ワンタイムパスワードが間違っているか、期限切れになっています。
+     */
+    "incorrectTotp": string;
     /**
      * 「{choice}」に投票しますか?
      */
diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml
index 62317cd5e6..92014c8abc 100644
--- a/locales/ja-JP.yml
+++ b/locales/ja-JP.yml
@@ -924,6 +924,7 @@ followersVisibility: "フォロワーの公開範囲"
 continueThread: "さらにスレッドを見る"
 deleteAccountConfirm: "アカウントが削除されます。よろしいですか?"
 incorrectPassword: "パスワードが間違っています。"
+incorrectTotp: "ワンタイムパスワードが間違っているか、期限切れになっています。"
 voteConfirm: "「{choice}」に投票しますか?"
 hide: "隠す"
 useDrawerReactionPickerForMobile: "モバイルデバイスのときドロワーで表示"
diff --git a/packages/backend/src/core/entities/UserEntityService.ts b/packages/backend/src/core/entities/UserEntityService.ts
index 69e2d6fc89..c9939adf11 100644
--- a/packages/backend/src/core/entities/UserEntityService.ts
+++ b/packages/backend/src/core/entities/UserEntityService.ts
@@ -545,11 +545,6 @@ export class UserEntityService implements OnModuleInit {
 				publicReactions: this.isLocalUser(user) ? profile!.publicReactions : false, // https://github.com/misskey-dev/misskey/issues/12964
 				followersVisibility: profile!.followersVisibility,
 				followingVisibility: profile!.followingVisibility,
-				twoFactorEnabled: profile!.twoFactorEnabled,
-				usePasswordLessLogin: profile!.usePasswordLessLogin,
-				securityKeys: profile!.twoFactorEnabled
-					? this.userSecurityKeysRepository.countBy({ userId: user.id }).then(result => result >= 1)
-					: false,
 				roles: this.roleService.getUserRoles(user.id).then(roles => roles.filter(role => role.isPublic).sort((a, b) => b.displayOrder - a.displayOrder).map(role => ({
 					id: role.id,
 					name: role.name,
@@ -564,6 +559,14 @@ export class UserEntityService implements OnModuleInit {
 				moderationNote: iAmModerator ? (profile!.moderationNote ?? '') : undefined,
 			} : {}),
 
+			...(isDetailed && (isMe || iAmModerator) ? {
+				twoFactorEnabled: profile!.twoFactorEnabled,
+				usePasswordLessLogin: profile!.usePasswordLessLogin,
+				securityKeys: profile!.twoFactorEnabled
+					? this.userSecurityKeysRepository.countBy({ userId: user.id }).then(result => result >= 1)
+					: false,
+			} : {}),
+
 			...(isDetailed && isMe ? {
 				avatarId: user.avatarId,
 				bannerId: user.bannerId,
diff --git a/packages/backend/src/models/json-schema/user.ts b/packages/backend/src/models/json-schema/user.ts
index 16c8a5a097..9cffd680f2 100644
--- a/packages/backend/src/models/json-schema/user.ts
+++ b/packages/backend/src/models/json-schema/user.ts
@@ -346,21 +346,6 @@ export const packedUserDetailedNotMeOnlySchema = {
 			nullable: false, optional: false,
 			enum: ['public', 'followers', 'private'],
 		},
-		twoFactorEnabled: {
-			type: 'boolean',
-			nullable: false, optional: false,
-			default: false,
-		},
-		usePasswordLessLogin: {
-			type: 'boolean',
-			nullable: false, optional: false,
-			default: false,
-		},
-		securityKeys: {
-			type: 'boolean',
-			nullable: false, optional: false,
-			default: false,
-		},
 		roles: {
 			type: 'array',
 			nullable: false, optional: false,
@@ -382,6 +367,18 @@ export const packedUserDetailedNotMeOnlySchema = {
 			type: 'string',
 			nullable: false, optional: true,
 		},
+		twoFactorEnabled: {
+			type: 'boolean',
+			nullable: false, optional: true,
+		},
+		usePasswordLessLogin: {
+			type: 'boolean',
+			nullable: false, optional: true,
+		},
+		securityKeys: {
+			type: 'boolean',
+			nullable: false, optional: true,
+		},
 		//#region relations
 		isFollowing: {
 			type: 'boolean',
@@ -630,6 +627,21 @@ export const packedMeDetailedOnlySchema = {
 			nullable: false, optional: false,
 			ref: 'RolePolicies',
 		},
+		twoFactorEnabled: {
+			type: 'boolean',
+			nullable: false, optional: false,
+			default: false,
+		},
+		usePasswordLessLogin: {
+			type: 'boolean',
+			nullable: false, optional: false,
+			default: false,
+		},
+		securityKeys: {
+			type: 'boolean',
+			nullable: false, optional: false,
+			default: false,
+		},
 		//#region secrets
 		email: {
 			type: 'string',
diff --git a/packages/backend/src/server/api/SigninApiService.ts b/packages/backend/src/server/api/SigninApiService.ts
index 2ccc75da00..81684beb3c 100644
--- a/packages/backend/src/server/api/SigninApiService.ts
+++ b/packages/backend/src/server/api/SigninApiService.ts
@@ -12,6 +12,7 @@ import type {
 	MiMeta,
 	SigninsRepository,
 	UserProfilesRepository,
+	UserSecurityKeysRepository,
 	UsersRepository,
 } from '@/models/_.js';
 import type { Config } from '@/config.js';
@@ -25,9 +26,27 @@ import { CaptchaService } from '@/core/CaptchaService.js';
 import { FastifyReplyError } from '@/misc/fastify-reply-error.js';
 import { RateLimiterService } from './RateLimiterService.js';
 import { SigninService } from './SigninService.js';
-import type { AuthenticationResponseJSON } from '@simplewebauthn/types';
+import type { AuthenticationResponseJSON, PublicKeyCredentialRequestOptionsJSON } from '@simplewebauthn/types';
 import type { FastifyReply, FastifyRequest } from 'fastify';
 
+/**
+ * next を指定すると、次にクライアント側で行うべき処理を指定できる。
+ *
+ * - `captcha`: パスワードと、(有効になっている場合は)CAPTCHAを求める
+ * - `password`: パスワードを求める
+ * - `totp`: ワンタイムパスワードを求める
+ * - `passkey`: WebAuthn認証を求める(WebAuthnに対応していないブラウザの場合はワンタイムパスワード)
+ */
+
+type SigninErrorResponse = {
+	id: string;
+	next?: 'captcha' | 'password' | 'totp';
+} | {
+	id: string;
+	next: 'passkey';
+	authRequest: PublicKeyCredentialRequestOptionsJSON;
+};
+
 @Injectable()
 export class SigninApiService {
 	constructor(
@@ -43,6 +62,9 @@ export class SigninApiService {
 		@Inject(DI.userProfilesRepository)
 		private userProfilesRepository: UserProfilesRepository,
 
+		@Inject(DI.userSecurityKeysRepository)
+		private userSecurityKeysRepository: UserSecurityKeysRepository,
+
 		@Inject(DI.signinsRepository)
 		private signinsRepository: SigninsRepository,
 
@@ -60,7 +82,7 @@ export class SigninApiService {
 		request: FastifyRequest<{
 			Body: {
 				username: string;
-				password: string;
+				password?: string;
 				token?: string;
 				credential?: AuthenticationResponseJSON;
 				'hcaptcha-response'?: string;
@@ -79,7 +101,7 @@ export class SigninApiService {
 		const password = body['password'];
 		const token = body['token'];
 
-		function error(status: number, error: { id: string }) {
+		function error(status: number, error: SigninErrorResponse) {
 			reply.code(status);
 			return { error };
 		}
@@ -103,11 +125,6 @@ export class SigninApiService {
 			return;
 		}
 
-		if (typeof password !== 'string') {
-			reply.code(400);
-			return;
-		}
-
 		if (token != null && typeof token !== 'string') {
 			reply.code(400);
 			return;
@@ -132,11 +149,36 @@ export class SigninApiService {
 		}
 
 		const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id });
+		const securityKeysAvailable = await this.userSecurityKeysRepository.countBy({ userId: user.id }).then(result => result >= 1);
+
+		if (password == null) {
+			reply.code(403);
+			if (profile.twoFactorEnabled) {
+				return {
+					error: {
+						id: '144ff4f8-bd6c-41bc-82c3-b672eb09efbf',
+						next: 'password',
+					},
+				} satisfies { error: SigninErrorResponse };
+			} else {
+				return {
+					error: {
+						id: '144ff4f8-bd6c-41bc-82c3-b672eb09efbf',
+						next: 'captcha',
+					},
+				} satisfies { error: SigninErrorResponse };
+			}
+		}
+
+		if (typeof password !== 'string') {
+			reply.code(400);
+			return;
+		}
 
 		// Compare password
 		const same = await bcrypt.compare(password, profile.password!);
 
-		const fail = async (status?: number, failure?: { id: string }) => {
+		const fail = async (status?: number, failure?: SigninErrorResponse) => {
 			// Append signin history
 			await this.signinsRepository.insert({
 				id: this.idService.gen(),
@@ -217,7 +259,7 @@ export class SigninApiService {
 					id: '93b86c4b-72f9-40eb-9815-798928603d1e',
 				});
 			}
-		} else {
+		} else if (securityKeysAvailable) {
 			if (!same && !profile.usePasswordLessLogin) {
 				return await fail(403, {
 					id: '932c904e-9460-45b7-9ce6-7ed33be7eb2c',
@@ -226,8 +268,28 @@ export class SigninApiService {
 
 			const authRequest = await this.webAuthnService.initiateAuthentication(user.id);
 
-			reply.code(200);
-			return authRequest;
+			reply.code(403);
+			return {
+				error: {
+					id: '06e661b9-8146-4ae3-bde5-47138c0ae0c4',
+					next: 'passkey',
+					authRequest,
+				},
+			} satisfies { error: SigninErrorResponse };
+		} else {
+			if (!same || !profile.twoFactorEnabled) {
+				return await fail(403, {
+					id: '932c904e-9460-45b7-9ce6-7ed33be7eb2c',
+				});
+			} else {
+				reply.code(403);
+				return {
+					error: {
+						id: '144ff4f8-bd6c-41bc-82c3-b672eb09efbf',
+						next: 'totp',
+					},
+				} satisfies { error: SigninErrorResponse };
+			}
 		}
 		// never get here
 	}
diff --git a/packages/backend/test/e2e/2fa.ts b/packages/backend/test/e2e/2fa.ts
index 06548fa7da..88c32b4346 100644
--- a/packages/backend/test/e2e/2fa.ts
+++ b/packages/backend/test/e2e/2fa.ts
@@ -136,13 +136,7 @@ describe('2要素認証', () => {
 		keyName: string,
 		credentialId: Buffer,
 		requestOptions: PublicKeyCredentialRequestOptionsJSON,
-	}): {
-		username: string,
-		password: string,
-		credential: AuthenticationResponseJSON,
-		'g-recaptcha-response'?: string | null,
-		'hcaptcha-response'?: string | null,
-	} => {
+	}): misskey.entities.SigninRequest => {
 		// AuthenticatorAssertionResponse.authenticatorData
 		// https://developer.mozilla.org/en-US/docs/Web/API/AuthenticatorAssertionResponse/authenticatorData
 		const authenticatorData = Buffer.concat([
@@ -202,11 +196,16 @@ describe('2要素認証', () => {
 		}, alice);
 		assert.strictEqual(doneResponse.status, 200);
 
-		const usersShowResponse = await api('users/show', {
-			username,
-		}, alice);
-		assert.strictEqual(usersShowResponse.status, 200);
-		assert.strictEqual((usersShowResponse.body as unknown as { twoFactorEnabled: boolean }).twoFactorEnabled, true);
+		const signinWithoutTokenResponse = await api('signin', {
+			...signinParam(),
+		});
+		assert.strictEqual(signinWithoutTokenResponse.status, 403);
+		assert.deepStrictEqual(signinWithoutTokenResponse.body, {
+			error: {
+				id: '144ff4f8-bd6c-41bc-82c3-b672eb09efbf',
+				next: 'totp',
+			},
+		});
 
 		const signinResponse = await api('signin', {
 			...signinParam(),
@@ -253,26 +252,28 @@ describe('2要素認証', () => {
 		assert.strictEqual(keyDoneResponse.body.id, credentialId.toString('base64url'));
 		assert.strictEqual(keyDoneResponse.body.name, keyName);
 
-		const usersShowResponse = await api('users/show', {
-			username,
-		});
-		assert.strictEqual(usersShowResponse.status, 200);
-		assert.strictEqual((usersShowResponse.body as unknown as { securityKeys: boolean }).securityKeys, true);
-
 		const signinResponse = await api('signin', {
 			...signinParam(),
 		});
-		assert.strictEqual(signinResponse.status, 200);
-		assert.strictEqual(signinResponse.body.i, undefined);
-		assert.notEqual((signinResponse.body as unknown as { challenge: unknown | undefined }).challenge, undefined);
-		assert.notEqual((signinResponse.body as unknown as { allowCredentials: unknown | undefined }).allowCredentials, undefined);
-		assert.strictEqual((signinResponse.body as unknown as { allowCredentials: {id: string}[] }).allowCredentials[0].id, credentialId.toString('base64url'));
+		const signinResponseBody = signinResponse.body as unknown as {
+			error: {
+				id: string;
+				next: 'passkey';
+				authRequest: PublicKeyCredentialRequestOptionsJSON;
+			};
+		};
+		assert.strictEqual(signinResponse.status, 403);
+		assert.strictEqual(signinResponseBody.error.id, '06e661b9-8146-4ae3-bde5-47138c0ae0c4');
+		assert.strictEqual(signinResponseBody.error.next, 'passkey');
+		assert.notEqual(signinResponseBody.error.authRequest.challenge, undefined);
+		assert.notEqual(signinResponseBody.error.authRequest.allowCredentials, undefined);
+		assert.strictEqual(signinResponseBody.error.authRequest.allowCredentials && signinResponseBody.error.authRequest.allowCredentials[0]?.id, credentialId.toString('base64url'));
 
 		const signinResponse2 = await api('signin', signinWithSecurityKeyParam({
 			keyName,
 			credentialId,
-			requestOptions: signinResponse.body,
-		} as any));
+			requestOptions: signinResponseBody.error.authRequest,
+		}));
 		assert.strictEqual(signinResponse2.status, 200);
 		assert.notEqual(signinResponse2.body.i, undefined);
 
@@ -315,24 +316,32 @@ describe('2要素認証', () => {
 		}, alice);
 		assert.strictEqual(passwordLessResponse.status, 204);
 
-		const usersShowResponse = await api('users/show', {
-			username,
-		});
-		assert.strictEqual(usersShowResponse.status, 200);
-		assert.strictEqual((usersShowResponse.body as unknown as { usePasswordLessLogin: boolean }).usePasswordLessLogin, true);
+		const iResponse = await api('i', {}, alice);
+		assert.strictEqual(iResponse.status, 200);
+		assert.strictEqual(iResponse.body.usePasswordLessLogin, true);
 
 		const signinResponse = await api('signin', {
 			...signinParam(),
 			password: '',
 		});
-		assert.strictEqual(signinResponse.status, 200);
-		assert.strictEqual(signinResponse.body.i, undefined);
+		const signinResponseBody = signinResponse.body as unknown as {
+			error: {
+				id: string;
+				next: 'passkey';
+				authRequest: PublicKeyCredentialRequestOptionsJSON;
+			};
+		};
+		assert.strictEqual(signinResponse.status, 403);
+		assert.strictEqual(signinResponseBody.error.id, '06e661b9-8146-4ae3-bde5-47138c0ae0c4');
+		assert.strictEqual(signinResponseBody.error.next, 'passkey');
+		assert.notEqual(signinResponseBody.error.authRequest.challenge, undefined);
+		assert.notEqual(signinResponseBody.error.authRequest.allowCredentials, undefined);
 
 		const signinResponse2 = await api('signin', {
 			...signinWithSecurityKeyParam({
 				keyName,
 				credentialId,
-				requestOptions: signinResponse.body,
+				requestOptions: signinResponseBody.error.authRequest,
 			} as any),
 			password: '',
 		});
@@ -424,11 +433,11 @@ describe('2要素認証', () => {
 		assert.strictEqual(keyDoneResponse.status, 200);
 
 		// テストの実行順によっては複数残ってるので全部消す
-		const iResponse = await api('i', {
+		const beforeIResponse = await api('i', {
 		}, alice);
-		assert.strictEqual(iResponse.status, 200);
-		assert.ok(iResponse.body.securityKeysList);
-		for (const key of iResponse.body.securityKeysList) {
+		assert.strictEqual(beforeIResponse.status, 200);
+		assert.ok(beforeIResponse.body.securityKeysList);
+		for (const key of beforeIResponse.body.securityKeysList) {
 			const removeKeyResponse = await api('i/2fa/remove-key', {
 				token: otpToken(registerResponse.body.secret),
 				password,
@@ -437,11 +446,9 @@ describe('2要素認証', () => {
 			assert.strictEqual(removeKeyResponse.status, 200);
 		}
 
-		const usersShowResponse = await api('users/show', {
-			username,
-		});
-		assert.strictEqual(usersShowResponse.status, 200);
-		assert.strictEqual((usersShowResponse.body as unknown as { securityKeys: boolean }).securityKeys, false);
+		const afterIResponse = await api('i', {}, alice);
+		assert.strictEqual(afterIResponse.status, 200);
+		assert.strictEqual(afterIResponse.body.securityKeys, false);
 
 		const signinResponse = await api('signin', {
 			...signinParam(),
@@ -468,11 +475,9 @@ describe('2要素認証', () => {
 		}, alice);
 		assert.strictEqual(doneResponse.status, 200);
 
-		const usersShowResponse = await api('users/show', {
-			username,
-		});
-		assert.strictEqual(usersShowResponse.status, 200);
-		assert.strictEqual((usersShowResponse.body as unknown as { twoFactorEnabled: boolean }).twoFactorEnabled, true);
+		const iResponse = await api('i', {}, alice);
+		assert.strictEqual(iResponse.status, 200);
+		assert.strictEqual(iResponse.body.twoFactorEnabled, true);
 
 		const unregisterResponse = await api('i/2fa/unregister', {
 			token: otpToken(registerResponse.body.secret),
diff --git a/packages/backend/test/e2e/users.ts b/packages/backend/test/e2e/users.ts
index 8ebe9af792..822ca14ae6 100644
--- a/packages/backend/test/e2e/users.ts
+++ b/packages/backend/test/e2e/users.ts
@@ -83,9 +83,6 @@ describe('ユーザー', () => {
 			publicReactions: user.publicReactions,
 			followingVisibility: user.followingVisibility,
 			followersVisibility: user.followersVisibility,
-			twoFactorEnabled: user.twoFactorEnabled,
-			usePasswordLessLogin: user.usePasswordLessLogin,
-			securityKeys: user.securityKeys,
 			roles: user.roles,
 			memo: user.memo,
 		});
@@ -149,6 +146,9 @@ describe('ユーザー', () => {
 			achievements: user.achievements,
 			loggedInDays: user.loggedInDays,
 			policies: user.policies,
+			twoFactorEnabled: user.twoFactorEnabled,
+			usePasswordLessLogin: user.usePasswordLessLogin,
+			securityKeys: user.securityKeys,
 			...(security ? {
 				email: user.email,
 				emailVerified: user.emailVerified,
@@ -343,9 +343,6 @@ describe('ユーザー', () => {
 		assert.strictEqual(response.publicReactions, true);
 		assert.strictEqual(response.followingVisibility, 'public');
 		assert.strictEqual(response.followersVisibility, 'public');
-		assert.strictEqual(response.twoFactorEnabled, false);
-		assert.strictEqual(response.usePasswordLessLogin, false);
-		assert.strictEqual(response.securityKeys, false);
 		assert.deepStrictEqual(response.roles, []);
 		assert.strictEqual(response.memo, null);
 
@@ -385,6 +382,9 @@ describe('ユーザー', () => {
 		assert.deepStrictEqual(response.achievements, []);
 		assert.deepStrictEqual(response.loggedInDays, 0);
 		assert.deepStrictEqual(response.policies, DEFAULT_POLICIES);
+		assert.strictEqual(response.twoFactorEnabled, false);
+		assert.strictEqual(response.usePasswordLessLogin, false);
+		assert.strictEqual(response.securityKeys, false);
 		assert.notStrictEqual(response.email, undefined);
 		assert.strictEqual(response.emailVerified, false);
 		assert.deepStrictEqual(response.securityKeysList, []);
@@ -618,6 +618,9 @@ describe('ユーザー', () => {
 		{ label: 'Moderatorになっている', user: () => userModerator, me: () => userModerator, selector: (user: misskey.entities.MeDetailed) => user.isModerator },
 		// @ts-expect-error UserDetailedNotMe doesn't include isModerator
 		{ label: '自分以外から見たときはModeratorか判定できない', user: () => userModerator, selector: (user: misskey.entities.UserDetailedNotMe) => user.isModerator, expected: () => undefined },
+		{ label: '自分から見た場合に二要素認証関連のプロパティがセットされている', user: () => alice, me: () => alice, selector: (user: misskey.entities.MeDetailed) => user.twoFactorEnabled, expected: () => false },
+		{ label: '自分以外から見た場合に二要素認証関連のプロパティがセットされていない', user: () => alice, me: () => bob, selector: (user: misskey.entities.UserDetailedNotMe) => user.twoFactorEnabled, expected: () => undefined },
+		{ label: 'モデレーターから見た場合に二要素認証関連のプロパティがセットされている', user: () => alice, me: () => userModerator, selector: (user: misskey.entities.UserDetailedNotMe) => user.twoFactorEnabled, expected: () => false },
 		{ label: 'サイレンスになっている', user: () => userSilenced, selector: (user: misskey.entities.UserDetailed) => user.isSilenced },
 		// FIXME: 落ちる
 		//{ label: 'サスペンドになっている', user: () => userSuspended, selector: (user: misskey.entities.UserDetailed) => user.isSuspended },
diff --git a/packages/frontend/src/components/MkSignin.input.vue b/packages/frontend/src/components/MkSignin.input.vue
new file mode 100644
index 0000000000..6336b78c80
--- /dev/null
+++ b/packages/frontend/src/components/MkSignin.input.vue
@@ -0,0 +1,206 @@
+<!--
+SPDX-FileCopyrightText: syuilo and misskey-project
+SPDX-License-Identifier: AGPL-3.0-only
+-->
+
+<template>
+<div :class="$style.wrapper" data-cy-signin-page-input>
+	<div :class="$style.root">
+		<div :class="$style.avatar">
+			<i class="ti ti-user"></i>
+		</div>
+
+		<!-- ログイン画面メッセージ -->
+		<MkInfo v-if="message">
+			{{ message }}
+		</MkInfo>
+
+		<!-- 外部サーバーへの転送 -->
+		<div v-if="openOnRemote" class="_gaps_m">
+			<div class="_gaps_s">
+				<MkButton type="button" rounded primary style="margin: 0 auto;" @click="openRemote(openOnRemote)">
+					{{ i18n.ts.continueOnRemote }} <i class="ti ti-external-link"></i>
+				</MkButton>
+				<button type="button" class="_button" :class="$style.instanceManualSelectButton" @click="specifyHostAndOpenRemote(openOnRemote)">
+					{{ i18n.ts.specifyServerHost }}
+				</button>
+			</div>
+			<div :class="$style.orHr">
+				<p :class="$style.orMsg">{{ i18n.ts.or }}</p>
+			</div>
+		</div>
+
+		<!-- username入力 -->
+		<form class="_gaps_s" @submit.prevent="emit('usernameSubmitted', username)">
+			<MkInput v-model="username" :placeholder="i18n.ts.username" type="text" pattern="^[a-zA-Z0-9_]+$" :spellcheck="false" autocomplete="username webauthn" autofocus required data-cy-signin-username>
+				<template #prefix>@</template>
+				<template #suffix>@{{ host }}</template>
+			</MkInput>
+			<MkButton type="submit" large primary rounded style="margin: 0 auto;" data-cy-signin-page-input-continue>{{ i18n.ts.continue }} <i class="ti ti-arrow-right"></i></MkButton>
+		</form>
+
+		<!-- パスワードレスログイン -->
+		<div :class="$style.orHr">
+			<p :class="$style.orMsg">{{ i18n.ts.or }}</p>
+		</div>
+		<div>
+			<MkButton type="submit" style="margin: auto auto;" large rounded primary gradate @click="emit('passkeyClick', $event)">
+				<i class="ti ti-device-usb" style="font-size: medium;"></i>{{ i18n.ts.signinWithPasskey }}
+			</MkButton>
+		</div>
+	</div>
+</div>
+</template>
+
+<script setup lang="ts">
+import { ref } from 'vue';
+import { toUnicode } from 'punycode/';
+
+import { query, extractDomain } from '@@/js/url.js';
+import { host as configHost } from '@@/js/config.js';
+import type { OpenOnRemoteOptions } from '@/scripts/please-login.js';
+import { i18n } from '@/i18n.js';
+import * as os from '@/os.js';
+
+import MkButton from '@/components/MkButton.vue';
+import MkInput from '@/components/MkInput.vue';
+import MkInfo from '@/components/MkInfo.vue';
+
+const props = withDefaults(defineProps<{
+	message?: string,
+	openOnRemote?: OpenOnRemoteOptions,
+}>(), {
+	message: '',
+	openOnRemote: undefined,
+});
+
+const emit = defineEmits<{
+	(ev: 'usernameSubmitted', v: string): void;
+	(ev: 'passkeyClick', v: MouseEvent): void;
+}>();
+
+const host = toUnicode(configHost);
+
+const username = ref('');
+
+//#region Open on remote
+function openRemote(options: OpenOnRemoteOptions, targetHost?: string): void {
+	switch (options.type) {
+		case 'web':
+		case 'lookup': {
+			let _path: string;
+
+			if (options.type === 'lookup') {
+				// TODO: v2024.7.0以降が浸透してきたら正式なURLに変更する▼
+				// _path = `/lookup?uri=${encodeURIComponent(_path)}`;
+				_path = `/authorize-follow?acct=${encodeURIComponent(options.url)}`;
+			} else {
+				_path = options.path;
+			}
+
+			if (targetHost) {
+				window.open(`https://${targetHost}${_path}`, '_blank', 'noopener');
+			} else {
+				window.open(`https://misskey-hub.net/mi-web/?path=${encodeURIComponent(_path)}`, '_blank', 'noopener');
+			}
+			break;
+		}
+		case 'share': {
+			const params = query(options.params);
+			if (targetHost) {
+				window.open(`https://${targetHost}/share?${params}`, '_blank', 'noopener');
+			} else {
+				window.open(`https://misskey-hub.net/share/?${params}`, '_blank', 'noopener');
+			}
+			break;
+		}
+	}
+}
+
+async function specifyHostAndOpenRemote(options: OpenOnRemoteOptions): Promise<void> {
+	const { canceled, result: hostTemp } = await os.inputText({
+		title: i18n.ts.inputHostName,
+		placeholder: 'misskey.example.com',
+	});
+
+	if (canceled) return;
+
+	let targetHost: string | null = hostTemp;
+
+	// ドメイン部分だけを取り出す
+	targetHost = extractDomain(targetHost ?? '');
+	if (targetHost == null) {
+		os.alert({
+			type: 'error',
+			title: i18n.ts.invalidValue,
+			text: i18n.ts.tryAgain,
+		});
+		return;
+	}
+	openRemote(options, targetHost);
+}
+//#endregion
+</script>
+
+<style lang="scss" module>
+.root {
+	display: flex;
+	flex-direction: column;
+	gap: 20px;
+}
+
+.wrapper {
+	display: flex;
+	align-items: center;
+	width: 100%;
+	min-height: 336px;
+
+	> .root {
+		width: 100%;
+	}
+}
+
+.avatar {
+	margin: 0 auto;
+	background-color: color-mix(in srgb, var(--fg), transparent 85%);
+	color: color-mix(in srgb, var(--fg), transparent 25%);
+	text-align: center;
+	height: 64px;
+	width: 64px;
+	font-size: 24px;
+	line-height: 64px;
+	border-radius: 50%;
+}
+
+.instanceManualSelectButton {
+	display: block;
+	text-align: center;
+	opacity: .7;
+	font-size: .8em;
+
+	&:hover {
+		text-decoration: underline;
+	}
+}
+
+.orHr {
+	position: relative;
+	margin: .4em auto;
+	width: 100%;
+	height: 1px;
+	background: var(--divider);
+}
+
+.orMsg {
+	position: absolute;
+	top: -.6em;
+	display: inline-block;
+	padding: 0 1em;
+	background: var(--panel);
+	font-size: 0.8em;
+	color: var(--fgOnPanel);
+	margin: 0;
+	left: 50%;
+	transform: translateX(-50%);
+}
+</style>
diff --git a/packages/frontend/src/components/MkSignin.passkey.vue b/packages/frontend/src/components/MkSignin.passkey.vue
new file mode 100644
index 0000000000..0d68955fab
--- /dev/null
+++ b/packages/frontend/src/components/MkSignin.passkey.vue
@@ -0,0 +1,92 @@
+<!--
+SPDX-FileCopyrightText: syuilo and misskey-project
+SPDX-License-Identifier: AGPL-3.0-only
+-->
+
+<template>
+<div :class="$style.wrapper">
+	<div class="_gaps" :class="$style.root">
+		<div class="_gaps_s">
+			<div :class="$style.passkeyIcon">
+				<i class="ti ti-fingerprint"></i>
+			</div>
+			<div :class="$style.passkeyDescription">{{ i18n.ts.useSecurityKey }}</div>
+		</div>
+
+		<MkButton large primary rounded :disabled="queryingKey" style="margin: 0 auto;" @click="queryKey">{{ i18n.ts.retry }}</MkButton>
+
+		<MkButton v-if="isPerformingPasswordlessLogin !== true" transparent rounded :disabled="queryingKey" style="margin: 0 auto;" @click="emit('useTotp')">{{ i18n.ts.useTotp }}</MkButton>
+	</div>
+</div>
+</template>
+
+<script setup lang="ts">
+import { ref, onMounted } from 'vue';
+import { get as webAuthnRequest } from '@github/webauthn-json/browser-ponyfill';
+
+import { i18n } from '@/i18n.js';
+
+import MkButton from '@/components/MkButton.vue';
+
+import type { AuthenticationPublicKeyCredential } from '@github/webauthn-json/browser-ponyfill';
+
+const props = defineProps<{
+	credentialRequest: CredentialRequestOptions;
+	isPerformingPasswordlessLogin?: boolean;
+}>();
+
+const emit = defineEmits<{
+	(ev: 'done', credential: AuthenticationPublicKeyCredential): void;
+	(ev: 'useTotp'): void;
+}>();
+
+const queryingKey = ref(true);
+
+async function queryKey() {
+	queryingKey.value = true;
+	await webAuthnRequest(props.credentialRequest)
+		.catch(() => {
+			return Promise.reject(null);
+		})
+		.then((credential) => {
+			emit('done', credential);
+		})
+		.finally(() => {
+			queryingKey.value = false;
+		});
+}
+
+onMounted(() => {
+	queryKey();
+});
+</script>
+
+<style lang="scss" module>
+.wrapper {
+	display: flex;
+	align-items: center;
+	width: 100%;
+	min-height: 336px;
+
+	> .root {
+		width: 100%;
+	}
+}
+
+.passkeyIcon {
+	margin: 0 auto;
+	background-color: var(--accentedBg);
+	color: var(--accent);
+	text-align: center;
+	height: 64px;
+	width: 64px;
+	font-size: 24px;
+	line-height: 64px;
+	border-radius: 50%;
+}
+
+.passkeyDescription {
+	text-align: center;
+	font-size: 1.1em;
+}
+</style>
diff --git a/packages/frontend/src/components/MkSignin.password.vue b/packages/frontend/src/components/MkSignin.password.vue
new file mode 100644
index 0000000000..2d79e2aeb1
--- /dev/null
+++ b/packages/frontend/src/components/MkSignin.password.vue
@@ -0,0 +1,181 @@
+<!--
+SPDX-FileCopyrightText: syuilo and misskey-project
+SPDX-License-Identifier: AGPL-3.0-only
+-->
+
+<template>
+<div :class="$style.wrapper" data-cy-signin-page-password>
+	<div class="_gaps" :class="$style.root">
+		<div :class="$style.avatar" :style="{ backgroundImage: user ? `url('${user.avatarUrl}')` : undefined }"></div>
+		<div :class="$style.welcomeBackMessage">
+			<I18n :src="i18n.ts.welcomeBackWithName" tag="span">
+				<template #name><Mfm :text="user.name ?? user.username" :plain="true"/></template>
+			</I18n>
+		</div>
+
+		<!-- password入力 -->
+		<form class="_gaps_s" @submit.prevent="onSubmit">
+			<!-- ブラウザ オートコンプリート用 -->
+			<input type="hidden" name="username" autocomplete="username" :value="user.username">
+
+			<MkInput v-model="password" :placeholder="i18n.ts.password" type="password" autocomplete="current-password webauthn" :withPasswordToggle="true" required autofocus data-cy-signin-password>
+				<template #prefix><i class="ti ti-lock"></i></template>
+				<template #caption><button class="_textButton" type="button" @click="resetPassword">{{ i18n.ts.forgotPassword }}</button></template>
+			</MkInput>
+
+			<div v-if="needCaptcha">
+				<MkCaptcha v-if="instance.enableHcaptcha" ref="hcaptcha" v-model="hCaptchaResponse" :class="$style.captcha" provider="hcaptcha" :sitekey="instance.hcaptchaSiteKey"/>
+				<MkCaptcha v-if="instance.enableMcaptcha" ref="mcaptcha" v-model="mCaptchaResponse" :class="$style.captcha" provider="mcaptcha" :sitekey="instance.mcaptchaSiteKey" :instanceUrl="instance.mcaptchaInstanceUrl"/>
+				<MkCaptcha v-if="instance.enableRecaptcha" ref="recaptcha" v-model="reCaptchaResponse" :class="$style.captcha" provider="recaptcha" :sitekey="instance.recaptchaSiteKey"/>
+				<MkCaptcha v-if="instance.enableTurnstile" ref="turnstile" v-model="turnstileResponse" :class="$style.captcha" provider="turnstile" :sitekey="instance.turnstileSiteKey"/>
+			</div>
+
+			<MkButton type="submit" :disabled="needCaptcha && captchaFailed" large primary rounded style="margin: 0 auto;" data-cy-signin-page-password-continue>{{ i18n.ts.continue }} <i class="ti ti-arrow-right"></i></MkButton>
+		</form>
+	</div>
+</div>
+</template>
+
+<script lang="ts">
+export type PwResponse = {
+	password: string;
+	captcha: {
+		hCaptchaResponse: string | null;
+		mCaptchaResponse: string | null;
+		reCaptchaResponse: string | null;
+		turnstileResponse: string | null;
+	};
+};
+</script>
+
+<script setup lang="ts">
+import { ref, computed, useTemplateRef, defineAsyncComponent } from 'vue';
+import * as Misskey from 'misskey-js';
+
+import { instance } from '@/instance.js';
+import { i18n } from '@/i18n.js';
+import * as os from '@/os.js';
+
+import MkButton from '@/components/MkButton.vue';
+import MkInput from '@/components/MkInput.vue';
+import MkCaptcha from '@/components/MkCaptcha.vue';
+
+const props = defineProps<{
+	user: Misskey.entities.UserDetailed;
+	needCaptcha: boolean;
+}>();
+
+const emit = defineEmits<{
+	(ev: 'passwordSubmitted', v: PwResponse): void;
+}>();
+
+const password = ref('');
+
+const hCaptcha = useTemplateRef('hcaptcha');
+const mCaptcha = useTemplateRef('mcaptcha');
+const reCaptcha = useTemplateRef('recaptcha');
+const turnstile = useTemplateRef('turnstile');
+
+const hCaptchaResponse = ref<string | null>(null);
+const mCaptchaResponse = ref<string | null>(null);
+const reCaptchaResponse = ref<string | null>(null);
+const turnstileResponse = ref<string | null>(null);
+
+const captchaFailed = computed((): boolean => {
+	return (
+		(instance.enableHcaptcha && !hCaptchaResponse.value) ||
+		(instance.enableMcaptcha && !mCaptchaResponse.value) ||
+		(instance.enableRecaptcha && !reCaptchaResponse.value) ||
+		(instance.enableTurnstile && !turnstileResponse.value)
+	);
+});
+
+function resetPassword(): void {
+	const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkForgotPassword.vue')), {}, {
+		closed: () => dispose(),
+	});
+}
+
+function onSubmit() {
+	emit('passwordSubmitted', {
+		password: password.value,
+		captcha: {
+			hCaptchaResponse: hCaptchaResponse.value,
+			mCaptchaResponse: mCaptchaResponse.value,
+			reCaptchaResponse: reCaptchaResponse.value,
+			turnstileResponse: turnstileResponse.value,
+		},
+	});
+}
+
+function resetCaptcha() {
+	hCaptcha.value?.reset();
+	mCaptcha.value?.reset();
+	reCaptcha.value?.reset();
+	turnstile.value?.reset();
+}
+
+defineExpose({
+	resetCaptcha,
+});
+</script>
+
+<style lang="scss" module>
+.wrapper {
+	display: flex;
+	align-items: center;
+	width: 100%;
+	min-height: 336px;
+
+	> .root {
+		width: 100%;
+	}
+}
+
+.avatar {
+	margin: 0 auto 0 auto;
+	width: 64px;
+	height: 64px;
+	background: #ddd;
+	background-position: center;
+	background-size: cover;
+	border-radius: 100%;
+}
+
+.welcomeBackMessage {
+	text-align: center;
+	font-size: 1.1em;
+}
+
+.instanceManualSelectButton {
+	display: block;
+	text-align: center;
+	opacity: .7;
+	font-size: .8em;
+
+	&:hover {
+		text-decoration: underline;
+	}
+}
+
+.orHr {
+	position: relative;
+	margin: .4em auto;
+	width: 100%;
+	height: 1px;
+	background: var(--divider);
+}
+
+.orMsg {
+	position: absolute;
+	top: -.6em;
+	display: inline-block;
+	padding: 0 1em;
+	background: var(--panel);
+	font-size: 0.8em;
+	color: var(--fgOnPanel);
+	margin: 0;
+	left: 50%;
+	transform: translateX(-50%);
+}
+</style>
diff --git a/packages/frontend/src/components/MkSignin.totp.vue b/packages/frontend/src/components/MkSignin.totp.vue
new file mode 100644
index 0000000000..880c08315e
--- /dev/null
+++ b/packages/frontend/src/components/MkSignin.totp.vue
@@ -0,0 +1,74 @@
+<!--
+SPDX-FileCopyrightText: syuilo and misskey-project
+SPDX-License-Identifier: AGPL-3.0-only
+-->
+
+<template>
+<div :class="$style.wrapper">
+	<div class="_gaps" :class="$style.root">
+		<div class="_gaps_s">
+			<div :class="$style.totpIcon">
+				<i class="ti ti-key"></i>
+			</div>
+			<div :class="$style.totpDescription">{{ i18n.ts['2fa'] }}</div>
+		</div>
+
+		<!-- totp入力 -->
+		<form class="_gaps_s" @submit.prevent="emit('totpSubmitted', token)">
+			<MkInput v-model="token" type="text" :pattern="isBackupCode ? '^[A-Z0-9]{32}$' :'^[0-9]{6}$'" autocomplete="one-time-code" required autofocus :spellcheck="false" :inputmode="isBackupCode ? undefined : 'numeric'">
+				<template #label>{{ i18n.ts.token }} ({{ i18n.ts['2fa'] }})</template>
+				<template #prefix><i v-if="isBackupCode" class="ti ti-key"></i><i v-else class="ti ti-123"></i></template>
+				<template #caption><button class="_textButton" type="button" @click="isBackupCode = !isBackupCode">{{ isBackupCode ? i18n.ts.useTotp : i18n.ts.useBackupCode }}</button></template>
+			</MkInput>
+
+			<MkButton type="submit" large primary rounded style="margin: 0 auto;">{{ i18n.ts.continue }} <i class="ti ti-arrow-right"></i></MkButton>
+		</form>
+	</div>
+</div>
+</template>
+
+<script setup lang="ts">
+import { ref } from 'vue';
+
+import { i18n } from '@/i18n.js';
+
+import MkButton from '@/components/MkButton.vue';
+import MkInput from '@/components/MkInput.vue';
+
+const emit = defineEmits<{
+	(ev: 'totpSubmitted', token: string): void;
+}>();
+
+const token = ref('');
+const isBackupCode = ref(false);
+</script>
+
+<style lang="scss" module>
+.wrapper {
+	display: flex;
+	align-items: center;
+	width: 100%;
+	min-height: 336px;
+
+	> .root {
+		width: 100%;
+	}
+}
+
+.totpIcon {
+	margin: 0 auto;
+	background-color: var(--accentedBg);
+	color: var(--accent);
+	text-align: center;
+	height: 64px;
+	width: 64px;
+	font-size: 24px;
+	line-height: 64px;
+	border-radius: 50%;
+}
+
+.totpDescription {
+	text-align: center;
+	font-size: 1.1em;
+}
+</style>
diff --git a/packages/frontend/src/components/MkSignin.vue b/packages/frontend/src/components/MkSignin.vue
index abbff8e1f2..81a98cae0e 100644
--- a/packages/frontend/src/components/MkSignin.vue
+++ b/packages/frontend/src/components/MkSignin.vue
@@ -4,438 +4,402 @@ SPDX-License-Identifier: AGPL-3.0-only
 -->
 
 <template>
-<form :class="{ signing, totpLogin }" @submit.prevent="onSubmit">
-	<div class="_gaps_m">
-		<div v-show="withAvatar" :class="$style.avatar" :style="{ backgroundImage: user ? `url('${user.avatarUrl}')` : undefined, marginBottom: message ? '1.5em' : undefined }"></div>
-		<MkInfo v-if="message">
-			{{ message }}
-		</MkInfo>
-		<div v-if="openOnRemote" class="_gaps_m">
-			<div class="_gaps_s">
-				<MkButton type="button" rounded primary style="margin: 0 auto;" @click="openRemote(openOnRemote)">
-					{{ i18n.ts.continueOnRemote }} <i class="ti ti-external-link"></i>
-				</MkButton>
-				<button type="button" class="_button" :class="$style.instanceManualSelectButton" @click="specifyHostAndOpenRemote(openOnRemote)">
-					{{ i18n.ts.specifyServerHost }}
-				</button>
-			</div>
-			<div :class="$style.orHr">
-				<p :class="$style.orMsg">{{ i18n.ts.or }}</p>
-			</div>
-		</div>
-		<div v-if="!totpLogin" class="normal-signin _gaps_m">
-			<MkInput v-model="username" :placeholder="i18n.ts.username" type="text" pattern="^[a-zA-Z0-9_]+$" :spellcheck="false" autocomplete="username webauthn" autofocus required data-cy-signin-username @update:modelValue="onUsernameChange">
-				<template #prefix>@</template>
-				<template #suffix>@{{ host }}</template>
-			</MkInput>
-			<MkInput v-model="password" :placeholder="i18n.ts.password" type="password" autocomplete="current-password webauthn" :withPasswordToggle="true" required data-cy-signin-password>
-				<template #prefix><i class="ti ti-lock"></i></template>
-				<template #caption><button class="_textButton" type="button" @click="resetPassword">{{ i18n.ts.forgotPassword }}</button></template>
-			</MkInput>
-			<MkCaptcha v-if="instance.enableHcaptcha" ref="hcaptcha" v-model="hCaptchaResponse" :class="$style.captcha" provider="hcaptcha" :sitekey="instance.hcaptchaSiteKey"/>
-			<MkCaptcha v-if="instance.enableMcaptcha" ref="mcaptcha" v-model="mCaptchaResponse" :class="$style.captcha" provider="mcaptcha" :sitekey="instance.mcaptchaSiteKey" :instanceUrl="instance.mcaptchaInstanceUrl"/>
-			<MkCaptcha v-if="instance.enableRecaptcha" ref="recaptcha" v-model="reCaptchaResponse" :class="$style.captcha" provider="recaptcha" :sitekey="instance.recaptchaSiteKey"/>
-			<MkCaptcha v-if="instance.enableTurnstile" ref="turnstile" v-model="turnstileResponse" :class="$style.captcha" provider="turnstile" :sitekey="instance.turnstileSiteKey"/>
-			<MkButton type="submit" large primary rounded :disabled="captchaFailed || signing" style="margin: 0 auto;">{{ signing ? i18n.ts.loggingIn : i18n.ts.login }}</MkButton>
-		</div>
-		<div v-if="totpLogin" class="2fa-signin" :class="{ securityKeys: user && user.securityKeys }">
-			<div v-if="user && user.securityKeys" class="twofa-group tap-group">
-				<p>{{ i18n.ts.useSecurityKey }}</p>
-				<MkButton v-if="!queryingKey" @click="query2FaKey">
-					{{ i18n.ts.retry }}
-				</MkButton>
-			</div>
-			<div v-if="user && user.securityKeys" :class="$style.orHr">
-				<p :class="$style.orMsg">{{ i18n.ts.or }}</p>
-			</div>
-			<div class="twofa-group totp-group _gaps">
-				<MkInput v-model="token" type="text" :pattern="isBackupCode ? '^[A-Z0-9]{32}$' :'^[0-9]{6}$'" autocomplete="one-time-code" required :spellcheck="false" :inputmode="isBackupCode ? undefined : 'numeric'">
-					<template #label>{{ i18n.ts.token }} ({{ i18n.ts['2fa'] }})</template>
-					<template #prefix><i v-if="isBackupCode" class="ti ti-key"></i><i v-else class="ti ti-123"></i></template>
-					<template #caption><button class="_textButton" type="button" @click="isBackupCode = !isBackupCode">{{ isBackupCode ? i18n.ts.useTotp : i18n.ts.useBackupCode }}</button></template>
-				</MkInput>
-				<MkButton type="submit" :disabled="signing" large primary rounded style="margin: 0 auto;">{{ signing ? i18n.ts.loggingIn : i18n.ts.login }}</MkButton>
-			</div>
-		</div>
-		<div v-if="!totpLogin && usePasswordLessLogin" :class="$style.orHr">
-			<p :class="$style.orMsg">{{ i18n.ts.or }}</p>
-		</div>
-		<div v-if="!totpLogin && usePasswordLessLogin" class="twofa-group tap-group">
-			<MkButton v-if="!queryingKey" type="submit" :disabled="signing" style="margin: auto auto;" rounded large primary @click="onPasskeyLogin">
-				<i class="ti ti-device-usb" style="font-size: medium;"></i>
-				{{ signing ? i18n.ts.loggingIn : i18n.ts.signinWithPasskey }}
-			</MkButton>
-			<p v-if="queryingKey">{{ i18n.ts.useSecurityKey }}</p>
-		</div>
+<div :class="$style.signinRoot">
+	<Transition
+		mode="out-in"
+		:enterActiveClass="$style.transition_enterActive"
+		:leaveActiveClass="$style.transition_leaveActive"
+		:enterFromClass="$style.transition_enterFrom"
+		:leaveToClass="$style.transition_leaveTo"
+
+		:inert="waiting"
+	>
+		<!-- 1. 外部サーバーへの転送・username入力・パスキー -->
+		<XInput
+			v-if="page === 'input'"
+			key="input"
+			:message="message"
+			:openOnRemote="openOnRemote"
+
+			@usernameSubmitted="onUsernameSubmitted"
+			@passkeyClick="onPasskeyLogin"
+		/>
+
+		<!-- 2. パスワード入力 -->
+		<XPassword
+			v-else-if="page === 'password'"
+			key="password"
+			ref="passwordPageEl"
+
+			:user="userInfo!"
+			:needCaptcha="needCaptcha"
+
+			@passwordSubmitted="onPasswordSubmitted"
+		/>
+
+		<!-- 3. ワンタイムパスワード -->
+		<XTotp
+			v-else-if="page === 'totp'"
+			key="totp"
+
+			@totpSubmitted="onTotpSubmitted"
+		/>
+
+		<!-- 4. パスキー -->
+		<XPasskey
+			v-else-if="page === 'passkey'"
+			key="passkey"
+
+			:credentialRequest="credentialRequest!"
+			:isPerformingPasswordlessLogin="doingPasskeyFromInputPage"
+
+			@done="onPasskeyDone"
+			@useTotp="onUseTotp"
+		/>
+	</Transition>
+	<div v-if="waiting" :class="$style.waitingRoot">
+		<MkLoading/>
 	</div>
-</form>
+</div>
 </template>
 
-<script lang="ts" setup>
-import { computed, defineAsyncComponent, ref } from 'vue';
-import { toUnicode } from 'punycode/';
+<script setup lang="ts">
+import { nextTick, onBeforeUnmount, ref, shallowRef, useTemplateRef } from 'vue';
 import * as Misskey from 'misskey-js';
-import { supported as webAuthnSupported, get as webAuthnRequest, parseRequestOptionsFromJSON } from '@github/webauthn-json/browser-ponyfill';
-import { query, extractDomain } from '@@/js/url.js';
-import { host as configHost } from '@@/js/config.js';
-import MkDivider from './MkDivider.vue';
-import type { OpenOnRemoteOptions } from '@/scripts/please-login.js';
-import { showSuspendedDialog } from '@/scripts/show-suspended-dialog.js';
-import MkButton from '@/components/MkButton.vue';
-import MkInput from '@/components/MkInput.vue';
-import MkInfo from '@/components/MkInfo.vue';
-import * as os from '@/os.js';
+import { supported as webAuthnSupported, parseRequestOptionsFromJSON } from '@github/webauthn-json/browser-ponyfill';
+
 import { misskeyApi } from '@/scripts/misskey-api.js';
+import { showSuspendedDialog } from '@/scripts/show-suspended-dialog.js';
 import { login } from '@/account.js';
 import { i18n } from '@/i18n.js';
-import { instance } from '@/instance.js';
-import MkCaptcha, { type Captcha } from '@/components/MkCaptcha.vue';
+import * as os from '@/os.js';
 
-const signing = ref(false);
-const user = ref<Misskey.entities.UserDetailed | null>(null);
-const usePasswordLessLogin = ref<Misskey.entities.UserDetailed['usePasswordLessLogin']>(true);
-const username = ref('');
-const password = ref('');
-const token = ref('');
-const host = ref(toUnicode(configHost));
-const totpLogin = ref(false);
-const isBackupCode = ref(false);
-const queryingKey = ref(false);
-let credentialRequest: CredentialRequestOptions | null = null;
-const passkey_context = ref('');
-const hcaptcha = ref<Captcha | undefined>();
-const mcaptcha = ref<Captcha | undefined>();
-const recaptcha = ref<Captcha | undefined>();
-const turnstile = ref<Captcha | undefined>();
-const hCaptchaResponse = ref<string | null>(null);
-const mCaptchaResponse = ref<string | null>(null);
-const reCaptchaResponse = ref<string | null>(null);
-const turnstileResponse = ref<string | null>(null);
+import XInput from '@/components/MkSignin.input.vue';
+import XPassword, { type PwResponse } from '@/components/MkSignin.password.vue';
+import XTotp from '@/components/MkSignin.totp.vue';
+import XPasskey from '@/components/MkSignin.passkey.vue';
 
-const captchaFailed = computed((): boolean => {
-	return (
-		instance.enableHcaptcha && !hCaptchaResponse.value ||
-		instance.enableMcaptcha && !mCaptchaResponse.value ||
-		instance.enableRecaptcha && !reCaptchaResponse.value ||
-		instance.enableTurnstile && !turnstileResponse.value);
-});
+import type { AuthenticationPublicKeyCredential } from '@github/webauthn-json/browser-ponyfill';
+import type { OpenOnRemoteOptions } from '@/scripts/please-login.js';
 
 const emit = defineEmits<{
-	(ev: 'login', v: any): void;
+	(ev: 'login', v: Misskey.entities.SigninResponse): void;
 }>();
 
 const props = withDefaults(defineProps<{
-	withAvatar?: boolean;
 	autoSet?: boolean;
 	message?: string,
 	openOnRemote?: OpenOnRemoteOptions,
 }>(), {
-	withAvatar: true,
 	autoSet: false,
 	message: '',
 	openOnRemote: undefined,
 });
 
-function onUsernameChange(): void {
-	misskeyApi('users/show', {
-		username: username.value,
-	}).then(userResponse => {
-		user.value = userResponse;
-		usePasswordLessLogin.value = userResponse.usePasswordLessLogin;
-	}, () => {
-		user.value = null;
-		usePasswordLessLogin.value = true;
-	});
-}
+const page = ref<'input' | 'password' | 'totp' | 'passkey'>('input');
+const waiting = ref(false);
 
-function onLogin(res: any): Promise<void> | void {
-	if (props.autoSet) {
-		return login(res.i);
-	}
-}
+const passwordPageEl = useTemplateRef('passwordPageEl');
+const needCaptcha = ref(false);
 
-async function query2FaKey(): Promise<void> {
-	if (credentialRequest == null) return;
-	queryingKey.value = true;
-	await webAuthnRequest(credentialRequest)
-		.catch(() => {
-			queryingKey.value = false;
-			return Promise.reject(null);
-		}).then(credential => {
-			credentialRequest = null;
-			queryingKey.value = false;
-			signing.value = true;
-			return misskeyApi('signin', {
-				username: username.value,
-				password: password.value,
-				credential: credential.toJSON(),
-			});
-		}).then(res => {
-			emit('login', res);
-			return onLogin(res);
-		}).catch(err => {
-			if (err === null) return;
-			os.alert({
-				type: 'error',
-				text: i18n.ts.signinFailed,
-			});
-			signing.value = false;
-		});
-}
+const userInfo = ref<null | Misskey.entities.UserDetailed>(null);
+const password = ref('');
+
+//#region Passkey Passwordless
+const credentialRequest = shallowRef<CredentialRequestOptions | null>(null);
+const passkeyContext = ref('');
+const doingPasskeyFromInputPage = ref(false);
 
 function onPasskeyLogin(): void {
-	signing.value = true;
 	if (webAuthnSupported()) {
+		doingPasskeyFromInputPage.value = true;
+		waiting.value = true;
 		misskeyApi('signin-with-passkey', {})
-			.then(res => {
-				totpLogin.value = false;
-				signing.value = false;
-				queryingKey.value = true;
-				passkey_context.value = res.context ?? '';
-				credentialRequest = parseRequestOptionsFromJSON({
+			.then((res) => {
+				passkeyContext.value = res.context ?? '';
+				credentialRequest.value = parseRequestOptionsFromJSON({
 					publicKey: res.option,
 				});
+
+				page.value = 'passkey';
+				waiting.value = false;
 			})
-			.then(() => queryPasskey())
-			.catch(loginFailed);
+			.catch(onLoginFailed);
 	}
 }
 
-async function queryPasskey(): Promise<void> {
-	if (credentialRequest == null) return;
-	queryingKey.value = true;
-	console.log('Waiting passkey auth...');
-	await webAuthnRequest(credentialRequest)
-		.catch((err) => {
-			console.warn('Passkey Auth fail!: ', err);
-			queryingKey.value = false;
-			return Promise.reject(null);
-		}).then(credential => {
-			credentialRequest = null;
-			queryingKey.value = false;
-			signing.value = true;
-			return misskeyApi('signin-with-passkey', {
-				credential: credential.toJSON(),
-				context: passkey_context.value,
-			});
-		}).then(res => {
+function onPasskeyDone(credential: AuthenticationPublicKeyCredential): void {
+	waiting.value = true;
+
+	if (doingPasskeyFromInputPage.value) {
+		misskeyApi('signin-with-passkey', {
+			credential: credential.toJSON(),
+			context: passkeyContext.value,
+		}).then((res) => {
+			if (res.signinResponse == null) {
+				onLoginFailed();
+				return;
+			}
 			emit('login', res.signinResponse);
-			return onLogin(res.signinResponse);
+		}).catch(onLoginFailed);
+	} else if (userInfo.value != null) {
+		tryLogin({
+			username: userInfo.value.username,
+			password: password.value,
+			credential: credential.toJSON(),
 		});
+	}
 }
 
-function onSubmit(): void {
-	signing.value = true;
-	if (!totpLogin.value && user.value && user.value.twoFactorEnabled) {
-		if (webAuthnSupported() && user.value.securityKeys) {
-			misskeyApi('signin', {
-				username: username.value,
-				password: password.value,
-			}).then(res => {
-				totpLogin.value = true;
-				signing.value = false;
-				credentialRequest = parseRequestOptionsFromJSON({
-					publicKey: res,
-				});
-			})
-				.then(() => query2FaKey())
-				.catch(loginFailed);
-		} else {
-			totpLogin.value = true;
-			signing.value = false;
+function onUseTotp(): void {
+	page.value = 'totp';
+}
+//#endregion
+
+async function onUsernameSubmitted(username: string) {
+	waiting.value = true;
+
+	userInfo.value = await misskeyApi('users/show', {
+		username,
+	}).catch(() => null);
+
+	await tryLogin({
+		username,
+	});
+}
+
+async function onPasswordSubmitted(pw: PwResponse) {
+	waiting.value = true;
+	password.value = pw.password;
+
+	if (userInfo.value == null) {
+		await os.alert({
+			type: 'error',
+			title: i18n.ts.noSuchUser,
+			text: i18n.ts.signinFailed,
+		});
+		waiting.value = false;
+		return;
+	} else {
+		await tryLogin({
+			username: userInfo.value.username,
+			password: pw.password,
+			'hcaptcha-response': pw.captcha.hCaptchaResponse,
+			'm-captcha-response': pw.captcha.mCaptchaResponse,
+			'g-recaptcha-response': pw.captcha.reCaptchaResponse,
+			'turnstile-response': pw.captcha.turnstileResponse,
+		});
+	}
+}
+
+async function onTotpSubmitted(token: string) {
+	waiting.value = true;
+
+	if (userInfo.value == null) {
+		await os.alert({
+			type: 'error',
+			title: i18n.ts.noSuchUser,
+			text: i18n.ts.signinFailed,
+		});
+		waiting.value = false;
+		return;
+	} else {
+		await tryLogin({
+			username: userInfo.value.username,
+			password: password.value,
+			token,
+		});
+	}
+}
+
+async function tryLogin(req: Partial<Misskey.entities.SigninRequest>): Promise<Misskey.entities.SigninResponse> {
+	const _req = {
+		username: req.username ?? userInfo.value?.username,
+		...req,
+	};
+
+	function assertIsSigninRequest(x: Partial<Misskey.entities.SigninRequest>): x is Misskey.entities.SigninRequest {
+		return x.username != null;
+	}
+
+	if (!assertIsSigninRequest(_req)) {
+		throw new Error('Invalid request');
+	}
+
+	return await misskeyApi('signin', _req).then(async (res) => {
+		emit('login', res);
+		await onLoginSucceeded(res);
+		return res;
+	}).catch((err) => {
+		onLoginFailed(err);
+		return Promise.reject(err);
+	});
+}
+
+async function onLoginSucceeded(res: Misskey.entities.SigninResponse) {
+	if (props.autoSet) {
+		await login(res.i);
+	}
+}
+
+function onLoginFailed(err?: any): void {
+	const id = err?.id ?? null;
+
+	if (typeof err === 'object' && 'next' in err) {
+		switch (err.next) {
+			case 'captcha': {
+				page.value = 'password';
+				break;
+			}
+			case 'password': {
+				page.value = 'password';
+				break;
+			}
+			case 'totp': {
+				page.value = 'totp';
+				break;
+			}
+			case 'passkey': {
+				if (webAuthnSupported() && 'authRequest' in err) {
+					credentialRequest.value = parseRequestOptionsFromJSON({
+						publicKey: err.authRequest,
+					});
+					page.value = 'passkey';
+				} else {
+					page.value = 'totp';
+				}
+				break;
+			}
 		}
 	} else {
-		misskeyApi('signin', {
-			username: username.value,
-			password: password.value,
-			'hcaptcha-response': hCaptchaResponse.value,
-			'm-captcha-response': mCaptchaResponse.value,
-			'g-recaptcha-response': reCaptchaResponse.value,
-			'turnstile-response': turnstileResponse.value,
-			token: user.value?.twoFactorEnabled ? token.value : undefined,
-		}).then(res => {
-			emit('login', res);
-			onLogin(res);
-		}).catch(loginFailed);
-	}
-}
-
-function loginFailed(err: any): void {
-	hcaptcha.value?.reset?.();
-	mcaptcha.value?.reset?.();
-	recaptcha.value?.reset?.();
-	turnstile.value?.reset?.();
-
-	switch (err.id) {
-		case '6cc579cc-885d-43d8-95c2-b8c7fc963280': {
-			os.alert({
-				type: 'error',
-				title: i18n.ts.loginFailed,
-				text: i18n.ts.noSuchUser,
-			});
-			break;
-		}
-		case '932c904e-9460-45b7-9ce6-7ed33be7eb2c': {
-			os.alert({
-				type: 'error',
-				title: i18n.ts.loginFailed,
-				text: i18n.ts.incorrectPassword,
-			});
-			break;
-		}
-		case 'e03a5f46-d309-4865-9b69-56282d94e1eb': {
-			showSuspendedDialog();
-			break;
-		}
-		case '22d05606-fbcf-421a-a2db-b32610dcfd1b': {
-			os.alert({
-				type: 'error',
-				title: i18n.ts.loginFailed,
-				text: i18n.ts.rateLimitExceeded,
-			});
-			break;
-		}
-		case '36b96a7d-b547-412d-aeed-2d611cdc8cdc': {
-			os.alert({
-				type: 'error',
-				title: i18n.ts.loginFailed,
-				text: i18n.ts.unknownWebAuthnKey,
-			});
-			break;
-		}
-		case 'b18c89a7-5b5e-4cec-bb5b-0419f332d430': {
-			os.alert({
-				type: 'error',
-				title: i18n.ts.loginFailed,
-				text: i18n.ts.passkeyVerificationFailed,
-			});
-			break;
-		}
-		case '2d84773e-f7b7-4d0b-8f72-bb69b584c912': {
-			os.alert({
-				type: 'error',
-				title: i18n.ts.loginFailed,
-				text: i18n.ts.passkeyVerificationSucceededButPasswordlessLoginDisabled,
-			});
-			break;
-		}
-		default: {
-			console.error(err);
-			os.alert({
-				type: 'error',
-				title: i18n.ts.loginFailed,
-				text: JSON.stringify(err),
-			});
+		switch (id) {
+			case '6cc579cc-885d-43d8-95c2-b8c7fc963280': {
+				os.alert({
+					type: 'error',
+					title: i18n.ts.loginFailed,
+					text: i18n.ts.noSuchUser,
+				});
+				break;
+			}
+			case '932c904e-9460-45b7-9ce6-7ed33be7eb2c': {
+				os.alert({
+					type: 'error',
+					title: i18n.ts.loginFailed,
+					text: i18n.ts.incorrectPassword,
+				});
+				break;
+			}
+			case 'e03a5f46-d309-4865-9b69-56282d94e1eb': {
+				showSuspendedDialog();
+				break;
+			}
+			case '22d05606-fbcf-421a-a2db-b32610dcfd1b': {
+				os.alert({
+					type: 'error',
+					title: i18n.ts.loginFailed,
+					text: i18n.ts.rateLimitExceeded,
+				});
+				break;
+			}
+			case 'cdf1235b-ac71-46d4-a3a6-84ccce48df6f': {
+				os.alert({
+					type: 'error',
+					title: i18n.ts.loginFailed,
+					text: i18n.ts.incorrectTotp,
+				});
+				break;
+			}
+			case '36b96a7d-b547-412d-aeed-2d611cdc8cdc': {
+				os.alert({
+					type: 'error',
+					title: i18n.ts.loginFailed,
+					text: i18n.ts.unknownWebAuthnKey,
+				});
+				break;
+			}
+			case '93b86c4b-72f9-40eb-9815-798928603d1e': {
+				os.alert({
+					type: 'error',
+					title: i18n.ts.loginFailed,
+					text: i18n.ts.passkeyVerificationFailed,
+				});
+				break;
+			}
+			case 'b18c89a7-5b5e-4cec-bb5b-0419f332d430': {
+				os.alert({
+					type: 'error',
+					title: i18n.ts.loginFailed,
+					text: i18n.ts.passkeyVerificationFailed,
+				});
+				break;
+			}
+			case '2d84773e-f7b7-4d0b-8f72-bb69b584c912': {
+				os.alert({
+					type: 'error',
+					title: i18n.ts.loginFailed,
+					text: i18n.ts.passkeyVerificationSucceededButPasswordlessLoginDisabled,
+				});
+				break;
+			}
+			default: {
+				console.error(err);
+				os.alert({
+					type: 'error',
+					title: i18n.ts.loginFailed,
+					text: JSON.stringify(err),
+				});
+			}
 		}
 	}
 
-	totpLogin.value = false;
-	signing.value = false;
-}
-
-function resetPassword(): void {
-	const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkForgotPassword.vue')), {}, {
-		closed: () => dispose(),
+	if (doingPasskeyFromInputPage.value === true) {
+		doingPasskeyFromInputPage.value = false;
+		page.value = 'input';
+		password.value = '';
+	}
+	passwordPageEl.value?.resetCaptcha();
+	nextTick(() => {
+		waiting.value = false;
 	});
 }
 
-function openRemote(options: OpenOnRemoteOptions, targetHost?: string): void {
-	switch (options.type) {
-		case 'web':
-		case 'lookup': {
-			let _path: string;
-
-			if (options.type === 'lookup') {
-				// TODO: v2024.7.0以降が浸透してきたら正式なURLに変更する▼
-				// _path = `/lookup?uri=${encodeURIComponent(_path)}`;
-				_path = `/authorize-follow?acct=${encodeURIComponent(options.url)}`;
-			} else {
-				_path = options.path;
-			}
-
-			if (targetHost) {
-				window.open(`https://${targetHost}${_path}`, '_blank', 'noopener');
-			} else {
-				window.open(`https://misskey-hub.net/mi-web/?path=${encodeURIComponent(_path)}`, '_blank', 'noopener');
-			}
-			break;
-		}
-		case 'share': {
-			const params = query(options.params);
-			if (targetHost) {
-				window.open(`https://${targetHost}/share?${params}`, '_blank', 'noopener');
-			} else {
-				window.open(`https://misskey-hub.net/share/?${params}`, '_blank', 'noopener');
-			}
-			break;
-		}
-	}
-}
-
-async function specifyHostAndOpenRemote(options: OpenOnRemoteOptions): Promise<void> {
-	const { canceled, result: hostTemp } = await os.inputText({
-		title: i18n.ts.inputHostName,
-		placeholder: 'misskey.example.com',
-	});
-
-	if (canceled) return;
-
-	let targetHost: string | null = hostTemp;
-
-	// ドメイン部分だけを取り出す
-	targetHost = extractDomain(targetHost);
-	if (targetHost == null) {
-		os.alert({
-			type: 'error',
-			title: i18n.ts.invalidValue,
-			text: i18n.ts.tryAgain,
-		});
-		return;
-	}
-	openRemote(options, targetHost);
-}
+onBeforeUnmount(() => {
+	password.value = '';
+	userInfo.value = null;
+});
 </script>
 
 <style lang="scss" module>
-.avatar {
-	margin: 0 auto 0 auto;
-	width: 64px;
-	height: 64px;
-	background: #ddd;
-	background-position: center;
-	background-size: cover;
-	border-radius: 100%;
+.transition_enterActive,
+.transition_leaveActive {
+	transition: opacity 0.3s cubic-bezier(0,0,.35,1), transform 0.3s cubic-bezier(0,0,.35,1);
+}
+.transition_enterFrom {
+	opacity: 0;
+	transform: translateX(50px);
+}
+.transition_leaveTo {
+	opacity: 0;
+	transform: translateX(-50px);
 }
 
-.instanceManualSelectButton {
-	display: block;
-	text-align: center;
-	opacity: .7;
-	font-size: .8em;
+.signinRoot {
+	overflow-x: hidden;
+	overflow-x: clip;
 
-	&:hover {
-		text-decoration: underline;
-	}
-}
-
-.orHr {
 	position: relative;
-	margin: .4em auto;
-	width: 100%;
-	height: 1px;
-	background: var(--divider);
 }
 
-.orMsg {
+.waitingRoot {
 	position: absolute;
-	top: -.6em;
-	display: inline-block;
-	padding: 0 1em;
-	background: var(--panel);
-	font-size: 0.8em;
-	color: var(--fgOnPanel);
-	margin: 0;
-	left: 50%;
-	transform: translateX(-50%);
+	top: 0;
+	left: 0;
+	width: 100%;
+	height: 100%;
+	background-color: color-mix(in srgb, var(--panel), transparent 50%);
+	display: flex;
+	justify-content: center;
+	align-items: center;
+	z-index: 1;
 }
 </style>
diff --git a/packages/frontend/src/components/MkSigninDialog.vue b/packages/frontend/src/components/MkSigninDialog.vue
index d48780e9de..8351d7d5e0 100644
--- a/packages/frontend/src/components/MkSigninDialog.vue
+++ b/packages/frontend/src/components/MkSigninDialog.vue
@@ -4,26 +4,29 @@ SPDX-License-Identifier: AGPL-3.0-only
 -->
 
 <template>
-<MkModalWindow
-	ref="dialog"
-	:width="400"
-	:height="450"
-	@close="onClose"
+<MkModal
+	ref="modal"
+	:preferType="'dialog'"
+	@click="onClose"
 	@closed="emit('closed')"
 >
-	<template #header>{{ i18n.ts.login }}</template>
-
-	<MkSpacer :marginMin="20" :marginMax="28">
-		<MkSignin :autoSet="autoSet" :message="message" :openOnRemote="openOnRemote" @login="onLogin"/>
-	</MkSpacer>
-</MkModalWindow>
+	<div :class="$style.root">
+		<div :class="$style.header">
+			<div :class="$style.headerText"><i class="ti ti-login-2"></i> {{ i18n.ts.login }}</div>
+			<button :class="$style.closeButton" class="_button" @click="onClose"><i class="ti ti-x"></i></button>
+		</div>
+		<div :class="$style.content">
+			<MkSignin :autoSet="autoSet" :message="message" :openOnRemote="openOnRemote" @login="onLogin"/>
+		</div>
+	</div>
+</MkModal>
 </template>
 
 <script lang="ts" setup>
 import { shallowRef } from 'vue';
 import type { OpenOnRemoteOptions } from '@/scripts/please-login.js';
 import MkSignin from '@/components/MkSignin.vue';
-import MkModalWindow from '@/components/MkModalWindow.vue';
+import MkModal from '@/components/MkModal.vue';
 import { i18n } from '@/i18n.js';
 
 withDefaults(defineProps<{
@@ -42,15 +45,62 @@ const emit = defineEmits<{
 	(ev: 'cancelled'): void;
 }>();
 
-const dialog = shallowRef<InstanceType<typeof MkModalWindow>>();
+const modal = shallowRef<InstanceType<typeof MkModal>>();
 
 function onClose() {
 	emit('cancelled');
-	if (dialog.value) dialog.value.close();
+	if (modal.value) modal.value.close();
 }
 
 function onLogin(res) {
 	emit('done', res);
-	if (dialog.value) dialog.value.close();
+	if (modal.value) modal.value.close();
 }
 </script>
+
+<style lang="scss" module>
+.root {
+	overflow: auto;
+	margin: auto;
+	position: relative;
+	width: 100%;
+	max-width: 400px;
+	height: 100%;
+	max-height: 450px;
+	box-sizing: border-box;
+	background: var(--panel);
+	border-radius: var(--radius);
+}
+
+.header {
+	position: sticky;
+	top: 0;
+	left: 0;
+	width: 100%;
+	height: 50px;
+	box-sizing: border-box;
+	display: flex;
+	align-items: center;
+	font-weight: bold;
+	backdrop-filter: var(--blur, blur(15px));
+	background: var(--acrylicBg);
+	z-index: 1;
+}
+
+.headerText {
+	padding: 0 20px;
+	box-sizing: border-box;
+}
+
+.closeButton {
+	margin-left: auto;
+	padding: 16px;
+	font-size: 16px;
+	line-height: 16px;
+}
+
+.content {
+	padding: 32px;
+	box-sizing: border-box;
+}
+</style>
diff --git a/packages/misskey-js/etc/misskey-js.api.md b/packages/misskey-js/etc/misskey-js.api.md
index 5f4792eb74..9ad784c296 100644
--- a/packages/misskey-js/etc/misskey-js.api.md
+++ b/packages/misskey-js/etc/misskey-js.api.md
@@ -3040,7 +3040,7 @@ type Signin = components['schemas']['Signin'];
 // @public (undocumented)
 type SigninRequest = {
     username: string;
-    password: string;
+    password?: string;
     token?: string;
     credential?: AuthenticationResponseJSON;
     'hcaptcha-response'?: string | null;
diff --git a/packages/misskey-js/src/autogen/types.ts b/packages/misskey-js/src/autogen/types.ts
index 32646d28ed..3876a0bfe5 100644
--- a/packages/misskey-js/src/autogen/types.ts
+++ b/packages/misskey-js/src/autogen/types.ts
@@ -3782,16 +3782,13 @@ export type components = {
       followingVisibility: 'public' | 'followers' | 'private';
       /** @enum {string} */
       followersVisibility: 'public' | 'followers' | 'private';
-      /** @default false */
-      twoFactorEnabled: boolean;
-      /** @default false */
-      usePasswordLessLogin: boolean;
-      /** @default false */
-      securityKeys: boolean;
       roles: components['schemas']['RoleLite'][];
       followedMessage?: string | null;
       memo: string | null;
       moderationNote?: string;
+      twoFactorEnabled?: boolean;
+      usePasswordLessLogin?: boolean;
+      securityKeys?: boolean;
       isFollowing?: boolean;
       isFollowed?: boolean;
       hasPendingFollowRequestFromYou?: boolean;
@@ -3972,6 +3969,12 @@ export type components = {
         }[];
       loggedInDays: number;
       policies: components['schemas']['RolePolicies'];
+      /** @default false */
+      twoFactorEnabled: boolean;
+      /** @default false */
+      usePasswordLessLogin: boolean;
+      /** @default false */
+      securityKeys: boolean;
       email?: string | null;
       emailVerified?: boolean | null;
       securityKeysList?: {
diff --git a/packages/misskey-js/src/entities.ts b/packages/misskey-js/src/entities.ts
index 36b7f5bca3..98ac50e5a1 100644
--- a/packages/misskey-js/src/entities.ts
+++ b/packages/misskey-js/src/entities.ts
@@ -269,7 +269,7 @@ export type SignupPendingResponse = {
 
 export type SigninRequest = {
 	username: string;
-	password: string;
+	password?: string;
 	token?: string;
 	credential?: AuthenticationResponseJSON;
 	'hcaptcha-response'?: string | null;

From 3b0b4f83dd36863d386ea9c45765f043ff866299 Mon Sep 17 00:00:00 2001
From: "github-actions[bot]" <github-actions[bot]@users.noreply.github.com>
Date: Fri, 4 Oct 2024 06:28:36 +0000
Subject: [PATCH 033/121] Bump version to 2024.10.0-beta.3

---
 package.json                     | 2 +-
 packages/misskey-js/package.json | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/package.json b/package.json
index 626b679a95..fd919d866f 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
 	"name": "misskey",
-	"version": "2024.10.0-beta.2",
+	"version": "2024.10.0-beta.3",
 	"codename": "nasubi",
 	"repository": {
 		"type": "git",
diff --git a/packages/misskey-js/package.json b/packages/misskey-js/package.json
index a5f96647ed..d9ee630faf 100644
--- a/packages/misskey-js/package.json
+++ b/packages/misskey-js/package.json
@@ -1,7 +1,7 @@
 {
 	"type": "module",
 	"name": "misskey-js",
-	"version": "2024.10.0-beta.2",
+	"version": "2024.10.0-beta.3",
 	"description": "Misskey SDK for JavaScript",
 	"license": "MIT",
 	"main": "./built/index.js",

From ea2675eaabe891e55702e0abd955650744feade5 Mon Sep 17 00:00:00 2001
From: syuilo <4439005+syuilo@users.noreply.github.com>
Date: Fri, 4 Oct 2024 16:41:08 +0900
Subject: [PATCH 034/121] =?UTF-8?q?fix(frontend):=20=E3=83=AA=E3=83=B3?=
 =?UTF-8?q?=E3=82=AF=E5=8B=95=E4=BD=9C=E3=81=AE=E3=82=AA=E3=83=BC=E3=83=90?=
 =?UTF-8?q?=E3=83=BC=E3=83=A9=E3=82=A4=E3=83=89=E3=81=8C=E5=8B=95=E4=BD=9C?=
 =?UTF-8?q?=E3=81=97=E3=81=A6=E3=81=84=E3=81=AA=E3=81=84=E3=81=AE=E3=82=92?=
 =?UTF-8?q?=E4=BF=AE=E6=AD=A3?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 packages/frontend-embed/src/components/EmCustomEmoji.vue | 2 --
 packages/frontend-embed/src/components/EmMfm.ts          | 9 +--------
 packages/frontend/src/components/global/MkMfm.ts         | 9 +++++++--
 3 files changed, 8 insertions(+), 12 deletions(-)

diff --git a/packages/frontend-embed/src/components/EmCustomEmoji.vue b/packages/frontend-embed/src/components/EmCustomEmoji.vue
index e4149cf363..59b670cdc6 100644
--- a/packages/frontend-embed/src/components/EmCustomEmoji.vue
+++ b/packages/frontend-embed/src/components/EmCustomEmoji.vue
@@ -38,8 +38,6 @@ const props = defineProps<{
 	host?: string | null;
 	url?: string;
 	useOriginalSize?: boolean;
-	menu?: boolean;
-	menuReaction?: boolean;
 	fallbackToImage?: boolean;
 }>();
 
diff --git a/packages/frontend-embed/src/components/EmMfm.ts b/packages/frontend-embed/src/components/EmMfm.ts
index b2bcf4597e..59f0d495e6 100644
--- a/packages/frontend-embed/src/components/EmMfm.ts
+++ b/packages/frontend-embed/src/components/EmMfm.ts
@@ -6,6 +6,7 @@
 import { VNode, h, SetupContext, provide } from 'vue';
 import * as mfm from 'mfm-js';
 import * as Misskey from 'misskey-js';
+import { host } from '@@/js/config.js';
 import EmUrl from '@/components/EmUrl.vue';
 import EmTime from '@/components/EmTime.vue';
 import EmLink from '@/components/EmLink.vue';
@@ -13,7 +14,6 @@ import EmMention from '@/components/EmMention.vue';
 import EmEmoji from '@/components/EmEmoji.vue';
 import EmCustomEmoji from '@/components/EmCustomEmoji.vue';
 import EmA from '@/components/EmA.vue';
-import { host } from '@@/js/config.js';
 
 function safeParseFloat(str: unknown): number | null {
 	if (typeof str !== 'string' || str === '') return null;
@@ -41,9 +41,6 @@ type MfmProps = {
 	rootScale?: number;
 	nyaize?: boolean | 'respect';
 	parsedNodes?: mfm.MfmNode[] | null;
-	enableEmojiMenu?: boolean;
-	enableEmojiMenuReaction?: boolean;
-	linkNavigationBehavior?: string;
 };
 
 type MfmEvents = {
@@ -52,8 +49,6 @@ type MfmEvents = {
 
 // eslint-disable-next-line import/no-default-export
 export default function (props: MfmProps, { emit }: { emit: SetupContext<MfmEvents>['emit'] }) {
-	provide('linkNavigationBehavior', props.linkNavigationBehavior);
-
 	const isNote = props.isNote ?? true;
 	const shouldNyaize = props.nyaize ? props.nyaize === 'respect' ? props.author?.isCat : false : false;
 
@@ -397,8 +392,6 @@ export default function (props: MfmProps, { emit }: { emit: SetupContext<MfmEven
 						normal: props.plain,
 						host: null,
 						useOriginalSize: scale >= 2.5,
-						menu: props.enableEmojiMenu,
-						menuReaction: props.enableEmojiMenuReaction,
 						fallbackToImage: false,
 					})];
 				} else {
diff --git a/packages/frontend/src/components/global/MkMfm.ts b/packages/frontend/src/components/global/MkMfm.ts
index d914492231..1beb8874e0 100644
--- a/packages/frontend/src/components/global/MkMfm.ts
+++ b/packages/frontend/src/components/global/MkMfm.ts
@@ -6,6 +6,7 @@
 import { VNode, h, SetupContext, provide } from 'vue';
 import * as mfm from 'mfm-js';
 import * as Misskey from 'misskey-js';
+import { host } from '@@/js/config.js';
 import MkUrl from '@/components/global/MkUrl.vue';
 import MkTime from '@/components/global/MkTime.vue';
 import MkLink from '@/components/MkLink.vue';
@@ -17,7 +18,6 @@ import MkCodeInline from '@/components/MkCodeInline.vue';
 import MkGoogle from '@/components/MkGoogle.vue';
 import MkSparkle from '@/components/MkSparkle.vue';
 import MkA, { MkABehavior } from '@/components/global/MkA.vue';
-import { host } from '@@/js/config.js';
 import { defaultStore } from '@/store.js';
 
 function safeParseFloat(str: unknown): number | null {
@@ -57,7 +57,8 @@ type MfmEvents = {
 
 // eslint-disable-next-line import/no-default-export
 export default function (props: MfmProps, { emit }: { emit: SetupContext<MfmEvents>['emit'] }) {
-	provide('linkNavigationBehavior', props.linkNavigationBehavior);
+	// こうしたいところだけど functional component 内では provide は使えない
+	//provide('linkNavigationBehavior', props.linkNavigationBehavior);
 
 	const isNote = props.isNote ?? true;
 	const shouldNyaize = props.nyaize ? props.nyaize === 'respect' ? props.author?.isCat : false : false;
@@ -350,6 +351,7 @@ export default function (props: MfmProps, { emit }: { emit: SetupContext<MfmEven
 					key: Math.random(),
 					url: token.props.url,
 					rel: 'nofollow noopener',
+					navigationBehavior: props.linkNavigationBehavior,
 				})];
 			}
 
@@ -358,6 +360,7 @@ export default function (props: MfmProps, { emit }: { emit: SetupContext<MfmEven
 					key: Math.random(),
 					url: token.props.url,
 					rel: 'nofollow noopener',
+					navigationBehavior: props.linkNavigationBehavior,
 				}, genEl(token.children, scale, true))];
 			}
 
@@ -366,6 +369,7 @@ export default function (props: MfmProps, { emit }: { emit: SetupContext<MfmEven
 					key: Math.random(),
 					host: (token.props.host == null && props.author && props.author.host != null ? props.author.host : token.props.host) ?? host,
 					username: token.props.username,
+					navigationBehavior: props.linkNavigationBehavior,
 				})];
 			}
 
@@ -374,6 +378,7 @@ export default function (props: MfmProps, { emit }: { emit: SetupContext<MfmEven
 					key: Math.random(),
 					to: isNote ? `/tags/${encodeURIComponent(token.props.hashtag)}` : `/user-tags/${encodeURIComponent(token.props.hashtag)}`,
 					style: 'color:var(--hashtag);',
+					behavior: props.linkNavigationBehavior,
 				}, `#${token.props.hashtag}`)];
 			}
 

From 2639e92e18df4337eb20e47fe038906c5561e13b Mon Sep 17 00:00:00 2001
From: syuilo <4439005+syuilo@users.noreply.github.com>
Date: Fri, 4 Oct 2024 17:07:27 +0900
Subject: [PATCH 035/121] :art:

---
 packages/frontend/src/pages/admin/modlog.vue | 7 ++++---
 1 file changed, 4 insertions(+), 3 deletions(-)

diff --git a/packages/frontend/src/pages/admin/modlog.vue b/packages/frontend/src/pages/admin/modlog.vue
index 8590ee1651..38610e7e92 100644
--- a/packages/frontend/src/pages/admin/modlog.vue
+++ b/packages/frontend/src/pages/admin/modlog.vue
@@ -20,9 +20,9 @@ SPDX-License-Identifier: AGPL-3.0-only
 			</div>
 
 			<MkPagination v-slot="{items}" ref="logs" :pagination="pagination" style="margin-top: var(--margin);">
-				<div class="_gaps_s">
-					<XModLog v-for="item in items" :key="item.id" :log="item"/>
-				</div>
+				<MkDateSeparatedList v-slot="{ item }" :items="items" :noGap="false" style="--margin: 8px;">
+					<XModLog :key="item.id" :log="item"/>
+				</MkDateSeparatedList>
 			</MkPagination>
 		</div>
 	</MkSpacer>
@@ -39,6 +39,7 @@ import MkInput from '@/components/MkInput.vue';
 import MkPagination from '@/components/MkPagination.vue';
 import { i18n } from '@/i18n.js';
 import { definePageMetadata } from '@/scripts/page-metadata.js';
+import MkDateSeparatedList from '@/components/MkDateSeparatedList.vue';
 
 const logs = shallowRef<InstanceType<typeof MkPagination>>();
 

From 708ffaef5c0ab8b7d7664cb3fae6420ac0e4cbcd Mon Sep 17 00:00:00 2001
From: syuilo <4439005+syuilo@users.noreply.github.com>
Date: Fri, 4 Oct 2024 17:29:10 +0900
Subject: [PATCH 036/121] :art:

---
 packages/frontend/src/pages/settings/apps.vue | 73 +++++++++----------
 1 file changed, 33 insertions(+), 40 deletions(-)

diff --git a/packages/frontend/src/pages/settings/apps.vue b/packages/frontend/src/pages/settings/apps.vue
index 0e0c1f4c0c..68e36ef1bb 100644
--- a/packages/frontend/src/pages/settings/apps.vue
+++ b/packages/frontend/src/pages/settings/apps.vue
@@ -14,30 +14,39 @@ SPDX-License-Identifier: AGPL-3.0-only
 		</template>
 		<template #default="{items}">
 			<div class="_gaps">
-				<div v-for="token in items" :key="token.id" class="_panel" :class="$style.app">
-					<img v-if="token.iconUrl" :class="$style.appIcon" :src="token.iconUrl" alt=""/>
-					<div :class="$style.appBody">
-						<div :class="$style.appName">{{ token.name }}</div>
-						<div>{{ token.description }}</div>
-						<MkKeyValue oneline>
-							<template #key>{{ i18n.ts.installedDate }}</template>
-							<template #value><MkTime :time="token.createdAt"/></template>
-						</MkKeyValue>
-						<MkKeyValue oneline>
-							<template #key>{{ i18n.ts.lastUsedDate }}</template>
-							<template #value><MkTime :time="token.lastUsedAt"/></template>
-						</MkKeyValue>
-						<details>
-							<summary>{{ i18n.ts.details }}</summary>
+				<MkFolder v-for="token in items" :key="token.id" :defaultOpen="true">
+					<template #icon>
+						<img v-if="token.iconUrl" :class="$style.appIcon" :src="token.iconUrl" alt=""/>
+						<i v-else class="ti ti-plug"/>
+					</template>
+					<template #label>{{ token.name }}</template>
+					<template #caption>{{ token.description }}</template>
+					<template #suffix><MkTime :time="token.lastUsedAt"/></template>
+					<template #footer>
+						<MkButton danger @click="revoke(token)"><i class="ti ti-trash"></i> {{ i18n.ts.delete }}</MkButton>
+					</template>
+
+					<div class="_gaps_s">
+						<div v-if="token.description">{{ token.description }}</div>
+						<div>
+							<MkKeyValue oneline>
+								<template #key>{{ i18n.ts.installedDate }}</template>
+								<template #value><MkTime :time="token.createdAt" :mode="'detail'"/></template>
+							</MkKeyValue>
+							<MkKeyValue oneline>
+								<template #key>{{ i18n.ts.lastUsedDate }}</template>
+								<template #value><MkTime :time="token.lastUsedAt" :mode="'detail'"/></template>
+							</MkKeyValue>
+						</div>
+						<MkFolder>
+							<template #label>{{ i18n.ts.permission }}</template>
+							<template #suffix>{{ Object.keys(token.permission).length === 0 ? i18n.ts.none : Object.keys(token.permission).length }}</template>
 							<ul>
 								<li v-for="p in token.permission" :key="p">{{ i18n.ts._permissions[p] }}</li>
 							</ul>
-						</details>
-						<div>
-							<MkButton inline danger @click="revoke(token)"><i class="ti ti-trash"></i></MkButton>
-						</div>
+						</MkFolder>
 					</div>
-				</div>
+				</MkFolder>
 			</div>
 		</template>
 	</FormPagination>
@@ -52,6 +61,7 @@ import { i18n } from '@/i18n.js';
 import { definePageMetadata } from '@/scripts/page-metadata.js';
 import MkKeyValue from '@/components/MkKeyValue.vue';
 import MkButton from '@/components/MkButton.vue';
+import MkFolder from '@/components/MkFolder.vue';
 import { infoImageUrl } from '@/instance.js';
 
 const list = ref<InstanceType<typeof FormPagination>>();
@@ -82,26 +92,9 @@ definePageMetadata(() => ({
 </script>
 
 <style lang="scss" module>
-.app {
-	display: flex;
-	padding: 16px;
-}
-
 .appIcon {
-	display: block;
-	flex-shrink: 0;
-	margin: 0 12px 0 0;
-	width: 50px;
-	height: 50px;
-	border-radius: 8px;
-}
-
-.appBody {
-	width: calc(100% - 62px);
-	position: relative;
-}
-
-.appName {
-	font-weight: bold;
+	width: 20px;
+	height: 20px;
+	border-radius: 4px;
 }
 </style>

From d8f30fb7938e41e8d4e62c5b7a9094ecefdebd44 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=8B=E3=81=A3=E3=81=93=E3=81=8B=E3=82=8A?=
 <67428053+kakkokari-gtyih@users.noreply.github.com>
Date: Fri, 4 Oct 2024 17:32:18 +0900
Subject: [PATCH 037/121] =?UTF-8?q?fix(frontend):=20canvas-confetti?=
 =?UTF-8?q?=E3=81=AE=E5=9E=8B=E5=AE=9A=E7=BE=A9=E3=82=92=E8=BF=BD=E5=8A=A0?=
 =?UTF-8?q?=20(#14692)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 packages/frontend/package.json |  3 +-
 pnpm-lock.yaml                 | 91 +++++++++++++---------------------
 2 files changed, 37 insertions(+), 57 deletions(-)

diff --git a/packages/frontend/package.json b/packages/frontend/package.json
index 11d7ff3963..3226a554a9 100644
--- a/packages/frontend/package.json
+++ b/packages/frontend/package.json
@@ -45,6 +45,7 @@
 		"date-fns": "2.30.0",
 		"estree-walker": "3.0.3",
 		"eventemitter3": "5.0.1",
+		"frontend-shared": "workspace:*",
 		"idb-keyval": "6.2.1",
 		"insert-text-at-cursor": "0.3.0",
 		"is-file-animated": "1.0.2",
@@ -54,7 +55,6 @@
 		"misskey-bubble-game": "workspace:*",
 		"misskey-js": "workspace:*",
 		"misskey-reversi": "workspace:*",
-		"frontend-shared": "workspace:*",
 		"photoswipe": "5.4.4",
 		"punycode": "2.3.1",
 		"rollup": "4.22.5",
@@ -96,6 +96,7 @@
 		"@storybook/vue3": "8.3.4",
 		"@storybook/vue3-vite": "8.3.4",
 		"@testing-library/vue": "8.1.0",
+		"@types/canvas-confetti": "^1.6.4",
 		"@types/estree": "1.0.6",
 		"@types/matter-js": "0.19.7",
 		"@types/micromatch": "4.0.9",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index b21a74cf57..1312e8c886 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -923,6 +923,9 @@ importers:
       '@testing-library/vue':
         specifier: 8.1.0
         version: 8.1.0(@vue/compiler-sfc@3.5.11)(@vue/server-renderer@3.5.11(vue@3.5.11(typescript@5.6.2)))(vue@3.5.11(typescript@5.6.2))
+      '@types/canvas-confetti':
+        specifier: ^1.6.4
+        version: 1.6.4
       '@types/estree':
         specifier: 1.0.6
         version: 1.0.6
@@ -1160,7 +1163,7 @@ importers:
         version: 7.17.0(eslint@9.11.0)(typescript@5.6.2)
       '@vitest/coverage-v8':
         specifier: 1.6.0
-        version: 1.6.0(vitest@1.6.0(@types/node@20.14.12)(happy-dom@10.0.3)(jsdom@24.1.1)(sass@1.79.4)(terser@5.33.0))
+        version: 1.6.0(vitest@1.6.0(@types/node@20.14.12)(happy-dom@10.0.3)(jsdom@24.1.1(bufferutil@4.0.8)(utf-8-validate@6.0.4))(sass@1.79.4)(terser@5.33.0))
       '@vue/runtime-core':
         specifier: 3.5.11
         version: 3.5.11
@@ -4588,6 +4591,9 @@ packages:
   '@types/cacheable-request@6.0.3':
     resolution: {integrity: sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==}
 
+  '@types/canvas-confetti@1.6.4':
+    resolution: {integrity: sha512-fNyZ/Fdw/Y92X0vv7B+BD6ysHL4xVU5dJcgzgxLdGbn8O3PezZNIJpml44lKM0nsGur+o/6+NZbZeNTt00U1uA==}
+
   '@types/color-convert@2.0.4':
     resolution: {integrity: sha512-Ub1MmDdyZ7mX//g25uBAoH/mWGd9swVbt8BseymnaE18SU4po/PjmCrHxqIIRjBo3hV/vh1KGr0eMxUhp+t+dQ==}
 
@@ -15528,6 +15534,8 @@ snapshots:
       '@types/node': 20.14.12
       '@types/responselike': 1.0.0
 
+  '@types/canvas-confetti@1.6.4': {}
+
   '@types/color-convert@2.0.4':
     dependencies:
       '@types/color-name': 1.1.1
@@ -15954,7 +15962,7 @@ snapshots:
       '@typescript-eslint/types': 7.17.0
       '@typescript-eslint/typescript-estree': 7.17.0(typescript@5.5.4)
       '@typescript-eslint/visitor-keys': 7.17.0
-      debug: 4.3.5(supports-color@8.1.1)
+      debug: 4.3.5(supports-color@5.5.0)
       eslint: 9.11.0
     optionalDependencies:
       typescript: 5.5.4
@@ -15967,7 +15975,7 @@ snapshots:
       '@typescript-eslint/types': 7.17.0
       '@typescript-eslint/typescript-estree': 7.17.0(typescript@5.6.2)
       '@typescript-eslint/visitor-keys': 7.17.0
-      debug: 4.3.5(supports-color@8.1.1)
+      debug: 4.3.5(supports-color@5.5.0)
       eslint: 9.11.0
     optionalDependencies:
       typescript: 5.6.2
@@ -15980,7 +15988,7 @@ snapshots:
       '@typescript-eslint/types': 7.17.0
       '@typescript-eslint/typescript-estree': 7.17.0(typescript@5.6.2)
       '@typescript-eslint/visitor-keys': 7.17.0
-      debug: 4.3.5(supports-color@8.1.1)
+      debug: 4.3.5(supports-color@5.5.0)
       eslint: 9.8.0
     optionalDependencies:
       typescript: 5.6.2
@@ -16001,7 +16009,7 @@ snapshots:
     dependencies:
       '@typescript-eslint/typescript-estree': 7.1.0(typescript@5.3.3)
       '@typescript-eslint/utils': 7.1.0(eslint@9.11.0)(typescript@5.3.3)
-      debug: 4.3.5(supports-color@8.1.1)
+      debug: 4.3.5(supports-color@5.5.0)
       eslint: 9.11.0
       ts-api-utils: 1.0.1(typescript@5.3.3)
     optionalDependencies:
@@ -16013,7 +16021,7 @@ snapshots:
     dependencies:
       '@typescript-eslint/typescript-estree': 7.17.0(typescript@5.5.4)
       '@typescript-eslint/utils': 7.17.0(eslint@9.11.0)(typescript@5.5.4)
-      debug: 4.3.5(supports-color@8.1.1)
+      debug: 4.3.5(supports-color@5.5.0)
       eslint: 9.11.0
       ts-api-utils: 1.3.0(typescript@5.5.4)
     optionalDependencies:
@@ -16025,7 +16033,7 @@ snapshots:
     dependencies:
       '@typescript-eslint/typescript-estree': 7.17.0(typescript@5.6.2)
       '@typescript-eslint/utils': 7.17.0(eslint@9.11.0)(typescript@5.6.2)
-      debug: 4.3.5(supports-color@8.1.1)
+      debug: 4.3.5(supports-color@5.5.0)
       eslint: 9.11.0
       ts-api-utils: 1.3.0(typescript@5.6.2)
     optionalDependencies:
@@ -16037,7 +16045,7 @@ snapshots:
     dependencies:
       '@typescript-eslint/typescript-estree': 7.17.0(typescript@5.6.2)
       '@typescript-eslint/utils': 7.17.0(eslint@9.8.0)(typescript@5.6.2)
-      debug: 4.3.5(supports-color@8.1.1)
+      debug: 4.3.5(supports-color@5.5.0)
       eslint: 9.8.0
       ts-api-utils: 1.3.0(typescript@5.6.2)
     optionalDependencies:
@@ -16053,7 +16061,7 @@ snapshots:
     dependencies:
       '@typescript-eslint/types': 7.1.0
       '@typescript-eslint/visitor-keys': 7.1.0
-      debug: 4.3.5(supports-color@8.1.1)
+      debug: 4.3.5(supports-color@5.5.0)
       globby: 11.1.0
       is-glob: 4.0.3
       minimatch: 9.0.3
@@ -16068,7 +16076,7 @@ snapshots:
     dependencies:
       '@typescript-eslint/types': 7.17.0
       '@typescript-eslint/visitor-keys': 7.17.0
-      debug: 4.3.5(supports-color@8.1.1)
+      debug: 4.3.5(supports-color@5.5.0)
       globby: 11.1.0
       is-glob: 4.0.3
       minimatch: 9.0.4
@@ -16083,7 +16091,7 @@ snapshots:
     dependencies:
       '@typescript-eslint/types': 7.17.0
       '@typescript-eslint/visitor-keys': 7.17.0
-      debug: 4.3.5(supports-color@8.1.1)
+      debug: 4.3.5(supports-color@5.5.0)
       globby: 11.1.0
       is-glob: 4.0.3
       minimatch: 9.0.4
@@ -16167,7 +16175,7 @@ snapshots:
     dependencies:
       '@ampproject/remapping': 2.2.1
       '@bcoe/v8-coverage': 0.2.3
-      debug: 4.3.5(supports-color@8.1.1)
+      debug: 4.3.5(supports-color@5.5.0)
       istanbul-lib-coverage: 3.2.2
       istanbul-lib-report: 3.0.1
       istanbul-lib-source-maps: 5.0.4
@@ -16182,11 +16190,11 @@ snapshots:
     transitivePeerDependencies:
       - supports-color
 
-  '@vitest/coverage-v8@1.6.0(vitest@1.6.0(@types/node@20.14.12)(happy-dom@10.0.3)(jsdom@24.1.1)(sass@1.79.4)(terser@5.33.0))':
+  '@vitest/coverage-v8@1.6.0(vitest@1.6.0(@types/node@20.14.12)(happy-dom@10.0.3)(jsdom@24.1.1(bufferutil@4.0.8)(utf-8-validate@6.0.4))(sass@1.79.4)(terser@5.33.0))':
     dependencies:
       '@ampproject/remapping': 2.2.1
       '@bcoe/v8-coverage': 0.2.3
-      debug: 4.3.5(supports-color@8.1.1)
+      debug: 4.3.5(supports-color@5.5.0)
       istanbul-lib-coverage: 3.2.2
       istanbul-lib-report: 3.0.1
       istanbul-lib-source-maps: 5.0.4
@@ -16197,7 +16205,7 @@ snapshots:
       std-env: 3.7.0
       strip-literal: 2.1.0
       test-exclude: 6.0.0
-      vitest: 1.6.0(@types/node@20.14.12)(happy-dom@10.0.3)(jsdom@24.1.1)(sass@1.79.4)(terser@5.33.0)
+      vitest: 1.6.0(@types/node@20.14.12)(happy-dom@10.0.3)(jsdom@24.1.1(bufferutil@4.0.8)(utf-8-validate@6.0.4))(sass@1.79.4)(terser@5.33.0)
     transitivePeerDependencies:
       - supports-color
 
@@ -16507,7 +16515,7 @@ snapshots:
 
   agent-base@7.1.0:
     dependencies:
-      debug: 4.3.5(supports-color@8.1.1)
+      debug: 4.3.5(supports-color@5.5.0)
     transitivePeerDependencies:
       - supports-color
 
@@ -19479,7 +19487,7 @@ snapshots:
   http-proxy-agent@7.0.2:
     dependencies:
       agent-base: 7.1.0
-      debug: 4.3.5(supports-color@8.1.1)
+      debug: 4.3.5(supports-color@5.5.0)
     transitivePeerDependencies:
       - supports-color
 
@@ -19518,7 +19526,7 @@ snapshots:
   https-proxy-agent@5.0.1:
     dependencies:
       agent-base: 6.0.2
-      debug: 4.3.5(supports-color@8.1.1)
+      debug: 4.3.5(supports-color@5.5.0)
     transitivePeerDependencies:
       - supports-color
     optional: true
@@ -19526,14 +19534,14 @@ snapshots:
   https-proxy-agent@7.0.2:
     dependencies:
       agent-base: 7.1.0
-      debug: 4.3.5(supports-color@8.1.1)
+      debug: 4.3.5(supports-color@5.5.0)
     transitivePeerDependencies:
       - supports-color
 
   https-proxy-agent@7.0.5:
     dependencies:
       agent-base: 7.1.0
-      debug: 4.3.5(supports-color@8.1.1)
+      debug: 4.3.5(supports-color@5.5.0)
     transitivePeerDependencies:
       - supports-color
 
@@ -19906,7 +19914,7 @@ snapshots:
   istanbul-lib-source-maps@5.0.4:
     dependencies:
       '@jridgewell/trace-mapping': 0.3.25
-      debug: 4.3.5(supports-color@8.1.1)
+      debug: 4.3.5(supports-color@5.5.0)
       istanbul-lib-coverage: 3.2.2
     transitivePeerDependencies:
       - supports-color
@@ -20307,35 +20315,6 @@ snapshots:
 
   jsdoc-type-pratt-parser@4.1.0: {}
 
-  jsdom@24.1.1:
-    dependencies:
-      cssstyle: 4.0.1
-      data-urls: 5.0.0
-      decimal.js: 10.4.3
-      form-data: 4.0.0
-      html-encoding-sniffer: 4.0.0
-      http-proxy-agent: 7.0.2
-      https-proxy-agent: 7.0.5
-      is-potential-custom-element-name: 1.0.1
-      nwsapi: 2.2.12
-      parse5: 7.1.2
-      rrweb-cssom: 0.7.1
-      saxes: 6.0.0
-      symbol-tree: 3.2.4
-      tough-cookie: 4.1.4
-      w3c-xmlserializer: 5.0.0
-      webidl-conversions: 7.0.0
-      whatwg-encoding: 3.1.1
-      whatwg-mimetype: 4.0.0
-      whatwg-url: 14.0.0
-      ws: 8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4)
-      xml-name-validator: 5.0.0
-    transitivePeerDependencies:
-      - bufferutil
-      - supports-color
-      - utf-8-validate
-    optional: true
-
   jsdom@24.1.1(bufferutil@4.0.7)(utf-8-validate@6.0.3):
     dependencies:
       cssstyle: 4.0.1
@@ -22910,7 +22889,7 @@ snapshots:
     dependencies:
       '@hapi/hoek': 11.0.4
       '@hapi/wreck': 18.0.1
-      debug: 4.3.5(supports-color@8.1.1)
+      debug: 4.3.5(supports-color@5.5.0)
       joi: 17.11.0
     transitivePeerDependencies:
       - supports-color
@@ -23870,7 +23849,7 @@ snapshots:
   vite-node@1.6.0(@types/node@20.14.12)(sass@1.79.3)(terser@5.33.0):
     dependencies:
       cac: 6.7.14
-      debug: 4.3.5(supports-color@8.1.1)
+      debug: 4.3.5(supports-color@5.5.0)
       pathe: 1.1.2
       picocolors: 1.0.1
       vite: 5.4.8(@types/node@20.14.12)(sass@1.79.3)(terser@5.33.0)
@@ -23888,7 +23867,7 @@ snapshots:
   vite-node@1.6.0(@types/node@20.14.12)(sass@1.79.4)(terser@5.33.0):
     dependencies:
       cac: 6.7.14
-      debug: 4.3.5(supports-color@8.1.1)
+      debug: 4.3.5(supports-color@5.5.0)
       pathe: 1.1.2
       picocolors: 1.0.1
       vite: 5.4.8(@types/node@20.14.12)(sass@1.79.4)(terser@5.33.0)
@@ -23970,7 +23949,7 @@ snapshots:
       - supports-color
       - terser
 
-  vitest@1.6.0(@types/node@20.14.12)(happy-dom@10.0.3)(jsdom@24.1.1)(sass@1.79.4)(terser@5.33.0):
+  vitest@1.6.0(@types/node@20.14.12)(happy-dom@10.0.3)(jsdom@24.1.1(bufferutil@4.0.8)(utf-8-validate@6.0.4))(sass@1.79.4)(terser@5.33.0):
     dependencies:
       '@vitest/expect': 1.6.0
       '@vitest/runner': 1.6.0
@@ -23995,7 +23974,7 @@ snapshots:
     optionalDependencies:
       '@types/node': 20.14.12
       happy-dom: 10.0.3
-      jsdom: 24.1.1
+      jsdom: 24.1.1(bufferutil@4.0.8)(utf-8-validate@6.0.4)
     transitivePeerDependencies:
       - less
       - lightningcss
@@ -24067,7 +24046,7 @@ snapshots:
 
   vue-eslint-parser@9.4.3(eslint@9.11.0):
     dependencies:
-      debug: 4.3.5(supports-color@8.1.1)
+      debug: 4.3.5(supports-color@5.5.0)
       eslint: 9.11.0
       eslint-scope: 7.2.2
       eslint-visitor-keys: 3.4.3

From 2340de035b250330d7d37179dee3929e9472c29b Mon Sep 17 00:00:00 2001
From: syuilo <4439005+syuilo@users.noreply.github.com>
Date: Fri, 4 Oct 2024 17:32:36 +0900
Subject: [PATCH 038/121] New Crowdin updates (#14677)

* New translations ja-jp.yml (Chinese Traditional)

* New translations ja-jp.yml (Korean)

* New translations ja-jp.yml (Chinese Simplified)

* New translations ja-jp.yml (English)

* New translations ja-jp.yml (Chinese Traditional)

* New translations ja-jp.yml (Korean)

* New translations ja-jp.yml (Chinese Simplified)

* New translations ja-jp.yml (Chinese Simplified)
---
 locales/en-US.yml | 5 +++++
 locales/ko-KR.yml | 4 ++++
 locales/zh-CN.yml | 5 +++++
 locales/zh-TW.yml | 4 ++++
 4 files changed, 18 insertions(+)

diff --git a/locales/en-US.yml b/locales/en-US.yml
index 7db3315f7d..7af6d65ea4 100644
--- a/locales/en-US.yml
+++ b/locales/en-US.yml
@@ -8,6 +8,9 @@ search: "Search"
 notifications: "Notifications"
 username: "Username"
 password: "Password"
+initialPasswordForSetup: "Initial password for setup"
+initialPasswordIsIncorrect: "Initial password for setup is incorrect"
+initialPasswordForSetupDescription: "Use the password you entered in the configuration file if you installed Misskey yourself.\n If you are using a Misskey hosting service, use the password provided.\n If you have not set a password, leave it blank to continue."
 forgotPassword: "Forgot password"
 fetchingAsApObject: "Fetching from the Fediverse..."
 ok: "OK"
@@ -1283,6 +1286,7 @@ signinWithPasskey: "Sign in with Passkey"
 unknownWebAuthnKey: "Unknown Passkey"
 passkeyVerificationFailed: "Passkey verification has failed."
 passkeyVerificationSucceededButPasswordlessLoginDisabled: "Passkey verification has succeeded but password-less login is disabled."
+messageToFollower: "Message to followers"
 _delivery:
   status: "Delivery status"
   stop: "Suspended"
@@ -2392,6 +2396,7 @@ _notification:
   followedBySomeUsers: "Followed by {n} users"
   flushNotification: "Clear notifications"
   exportOfXCompleted: "Export of {x} has been completed"
+  login: "Someone logged in"
   _types:
     all: "All"
     note: "New notes"
diff --git a/locales/ko-KR.yml b/locales/ko-KR.yml
index 76ad982056..b85bc048e1 100644
--- a/locales/ko-KR.yml
+++ b/locales/ko-KR.yml
@@ -8,6 +8,9 @@ search: "검색"
 notifications: "알림"
 username: "유저명"
 password: "비밀번호"
+initialPasswordForSetup: "초기 설정용 비밀번호"
+initialPasswordIsIncorrect: "초기 설정용 비밀번호가 올바르지 않습니다."
+initialPasswordForSetupDescription: "Misskey를 직접 설치하는 경우, 설정 파일에 입력해둔 비밀번호를 사용하세요.\nMisskey 설치를 도와주는 호스팅 서비스 등을 사용하는 경우, 서비스 제공자로부터 받은 비밀번호를 사용하세요.\n비밀번호를 따로 설정하지 않은 경우, 아무것도 입력하지 않아도 됩니다."
 forgotPassword: "비밀번호 재설정"
 fetchingAsApObject: "연합에서 찾아보는 중"
 ok: "확인"
@@ -2393,6 +2396,7 @@ _notification:
   followedBySomeUsers: "{n}명에게 팔로우됨"
   flushNotification: "알림 이력을 초기화"
   exportOfXCompleted: "{x} 추출에 성공했습니다."
+  login: "로그인 알림이 있습니다"
   _types:
     all: "전부"
     note: "사용자의 새 글"
diff --git a/locales/zh-CN.yml b/locales/zh-CN.yml
index 7b2037d076..15f84e845d 100644
--- a/locales/zh-CN.yml
+++ b/locales/zh-CN.yml
@@ -8,6 +8,9 @@ search: "搜索"
 notifications: "通知"
 username: "用户名"
 password: "密码"
+initialPasswordForSetup: "初始化密码"
+initialPasswordIsIncorrect: "初始化密码不正确"
+initialPasswordForSetupDescription: "如果是自己安装的 Misskey,请输入配置文件里设好的密码。\n如果使用的是 Misskey 的托管服务等,请输入服务商提供的密码。\n如果没有设置密码,请留空并继续。"
 forgotPassword: "忘记密码"
 fetchingAsApObject: "在联邦宇宙查询中..."
 ok: "OK"
@@ -921,6 +924,7 @@ followersVisibility: "关注者的公开范围"
 continueThread: "查看更多帖子"
 deleteAccountConfirm: "将要删除账户。是否确认?"
 incorrectPassword: "密码错误"
+incorrectTotp: "一次性密码不正确或已过期"
 voteConfirm: "确定投给 “{choice}” ?"
 hide: "隐藏"
 useDrawerReactionPickerForMobile: "在移动设备上使用抽屉显示"
@@ -2393,6 +2397,7 @@ _notification:
   followedBySomeUsers: "被 {n} 人关注"
   flushNotification: "重置通知历史"
   exportOfXCompleted: "已完成 {x} 个导出"
+  login: "有新的登录"
   _types:
     all: "全部"
     note: "用户的新帖子"
diff --git a/locales/zh-TW.yml b/locales/zh-TW.yml
index f73bba6664..6659efcb7a 100644
--- a/locales/zh-TW.yml
+++ b/locales/zh-TW.yml
@@ -8,6 +8,9 @@ search: "搜尋"
 notifications: "通知"
 username: "使用者名稱"
 password: "密碼"
+initialPasswordForSetup: "初始設定用的密碼"
+initialPasswordIsIncorrect: "初始設定用的密碼錯誤。"
+initialPasswordForSetupDescription: "如果您自己安裝了 Misskey,請使用您在設定檔中輸入的密碼。\n如果您使用 Misskey 的託管服務之類的服務,請使用提供的密碼。\n如果您尚未設定密碼,請將其留空並繼續。"
 forgotPassword: "忘記密碼"
 fetchingAsApObject: "從聯邦宇宙取得中..."
 ok: "OK"
@@ -2393,6 +2396,7 @@ _notification:
   followedBySomeUsers: "被{n}人追隨了"
   flushNotification: "重置通知歷史紀錄"
   exportOfXCompleted: "{x} 的匯出已完成。"
+  login: "已登入"
   _types:
     all: "全部 "
     note: "使用者的最新貼文"

From 3d637af65b4f4fa7e557231aa9790bb87211b4e9 Mon Sep 17 00:00:00 2001
From: "github-actions[bot]" <github-actions[bot]@users.noreply.github.com>
Date: Fri, 4 Oct 2024 08:41:30 +0000
Subject: [PATCH 039/121] Bump version to 2024.10.0-beta.4

---
 package.json                     | 2 +-
 packages/misskey-js/package.json | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/package.json b/package.json
index fd919d866f..7c01180531 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
 	"name": "misskey",
-	"version": "2024.10.0-beta.3",
+	"version": "2024.10.0-beta.4",
 	"codename": "nasubi",
 	"repository": {
 		"type": "git",
diff --git a/packages/misskey-js/package.json b/packages/misskey-js/package.json
index d9ee630faf..4643516b7b 100644
--- a/packages/misskey-js/package.json
+++ b/packages/misskey-js/package.json
@@ -1,7 +1,7 @@
 {
 	"type": "module",
 	"name": "misskey-js",
-	"version": "2024.10.0-beta.3",
+	"version": "2024.10.0-beta.4",
 	"description": "Misskey SDK for JavaScript",
 	"license": "MIT",
 	"main": "./built/index.js",

From b36d13d90ca7835f385cb744f2b6a94d05220d09 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=8B=E3=81=A3=E3=81=93=E3=81=8B=E3=82=8A?=
 <67428053+kakkokari-gtyih@users.noreply.github.com>
Date: Fri, 4 Oct 2024 18:45:03 +0900
Subject: [PATCH 040/121] =?UTF-8?q?fix(frontend):=20=E3=83=AD=E3=82=B0?=
 =?UTF-8?q?=E3=82=A4=E3=83=B3=E7=94=BB=E9=9D=A2=E3=81=A7=E3=82=AD=E3=83=A3?=
 =?UTF-8?q?=E3=83=97=E3=83=81=E3=83=A3=E3=81=8C=E8=A1=A8=E7=A4=BA=E3=81=95?=
 =?UTF-8?q?=E3=82=8C=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(#14694)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

* fix(frontend): ログイン画面でキャプチャが表示されない問題を修正

* rename
---
 packages/frontend/src/components/MkSignin.vue | 13 ++++++++-----
 1 file changed, 8 insertions(+), 5 deletions(-)

diff --git a/packages/frontend/src/components/MkSignin.vue b/packages/frontend/src/components/MkSignin.vue
index 81a98cae0e..03dd61f6c6 100644
--- a/packages/frontend/src/components/MkSignin.vue
+++ b/packages/frontend/src/components/MkSignin.vue
@@ -124,7 +124,7 @@ function onPasskeyLogin(): void {
 				page.value = 'passkey';
 				waiting.value = false;
 			})
-			.catch(onLoginFailed);
+			.catch(onSigninApiError);
 	}
 }
 
@@ -137,11 +137,11 @@ function onPasskeyDone(credential: AuthenticationPublicKeyCredential): void {
 			context: passkeyContext.value,
 		}).then((res) => {
 			if (res.signinResponse == null) {
-				onLoginFailed();
+				onSigninApiError();
 				return;
 			}
 			emit('login', res.signinResponse);
-		}).catch(onLoginFailed);
+		}).catch(onSigninApiError);
 	} else if (userInfo.value != null) {
 		tryLogin({
 			username: userInfo.value.username,
@@ -231,7 +231,7 @@ async function tryLogin(req: Partial<Misskey.entities.SigninRequest>): Promise<M
 		await onLoginSucceeded(res);
 		return res;
 	}).catch((err) => {
-		onLoginFailed(err);
+		onSigninApiError(err);
 		return Promise.reject(err);
 	});
 }
@@ -242,16 +242,18 @@ async function onLoginSucceeded(res: Misskey.entities.SigninResponse) {
 	}
 }
 
-function onLoginFailed(err?: any): void {
+function onSigninApiError(err?: any): void {
 	const id = err?.id ?? null;
 
 	if (typeof err === 'object' && 'next' in err) {
 		switch (err.next) {
 			case 'captcha': {
+				needCaptcha.value = true;
 				page.value = 'password';
 				break;
 			}
 			case 'password': {
+				needCaptcha.value = false;
 				page.value = 'password';
 				break;
 			}
@@ -365,6 +367,7 @@ function onLoginFailed(err?: any): void {
 
 onBeforeUnmount(() => {
 	password.value = '';
+	needCaptcha.value = false;
 	userInfo.value = null;
 });
 </script>

From fa06c59eaee5b7efeabf081b8a380390a2a1cd83 Mon Sep 17 00:00:00 2001
From: syuilo <4439005+syuilo@users.noreply.github.com>
Date: Fri, 4 Oct 2024 19:09:46 +0900
Subject: [PATCH 041/121] :art:

---
 .../frontend/src/pages/settings/profile.vue   | 39 +++++++------------
 1 file changed, 15 insertions(+), 24 deletions(-)

diff --git a/packages/frontend/src/pages/settings/profile.vue b/packages/frontend/src/pages/settings/profile.vue
index 9e6cd04365..19c5d892de 100644
--- a/packages/frontend/src/pages/settings/profile.vue
+++ b/packages/frontend/src/pages/settings/profile.vue
@@ -46,14 +46,17 @@ SPDX-License-Identifier: AGPL-3.0-only
 		<MkFolder>
 			<template #icon><i class="ti ti-list"></i></template>
 			<template #label>{{ i18n.ts._profile.metadataEdit }}</template>
-
-			<div :class="$style.metadataRoot">
-				<div :class="$style.metadataMargin">
-					<MkButton :disabled="fields.length >= 16" inline style="margin-right: 8px;" @click="addField"><i class="ti ti-plus"></i> {{ i18n.ts.add }}</MkButton>
-					<MkButton v-if="!fieldEditMode" :disabled="fields.length <= 1" inline danger style="margin-right: 8px;" @click="fieldEditMode = !fieldEditMode"><i class="ti ti-trash"></i> {{ i18n.ts.delete }}</MkButton>
-					<MkButton v-else inline style="margin-right: 8px;" @click="fieldEditMode = !fieldEditMode"><i class="ti ti-arrows-sort"></i> {{ i18n.ts.rearrange }}</MkButton>
-					<MkButton inline primary @click="saveFields"><i class="ti ti-check"></i> {{ i18n.ts.save }}</MkButton>
+			<template #footer>
+				<div class="_buttons">
+					<MkButton primary @click="saveFields"><i class="ti ti-check"></i> {{ i18n.ts.save }}</MkButton>
+					<MkButton :disabled="fields.length >= 16" @click="addField"><i class="ti ti-plus"></i> {{ i18n.ts.add }}</MkButton>
+					<MkButton v-if="!fieldEditMode" :disabled="fields.length <= 1" danger @click="fieldEditMode = !fieldEditMode"><i class="ti ti-trash"></i> {{ i18n.ts.delete }}</MkButton>
+					<MkButton v-else @click="fieldEditMode = !fieldEditMode"><i class="ti ti-arrows-sort"></i> {{ i18n.ts.rearrange }}</MkButton>
 				</div>
+			</template>
+
+			<div :class="$style.metadataRoot" class="_gaps_s">
+				<MkInfo>{{ i18n.ts._profile.verifiedLinkDescription }}</MkInfo>
 
 				<Sortable
 					v-model="fields"
@@ -65,24 +68,20 @@ SPDX-License-Identifier: AGPL-3.0-only
 					@end="e => e.item.classList.remove('active')"
 				>
 					<template #item="{element, index}">
-						<div :class="$style.fieldDragItem">
+						<div v-panel :class="$style.fieldDragItem">
 							<button v-if="!fieldEditMode" class="_button" :class="$style.dragItemHandle" tabindex="-1"><i class="ti ti-menu"></i></button>
 							<button v-if="fieldEditMode" :disabled="fields.length <= 1" class="_button" :class="$style.dragItemRemove" @click="deleteField(index)"><i class="ti ti-x"></i></button>
 							<div :class="$style.dragItemForm">
 								<FormSplit :minWidth="200">
-									<MkInput v-model="element.name" small>
-										<template #label>{{ i18n.ts._profile.metadataLabel }}</template>
+									<MkInput v-model="element.name" small :placeholder="i18n.ts._profile.metadataLabel">
 									</MkInput>
-									<MkInput v-model="element.value" small>
-										<template #label>{{ i18n.ts._profile.metadataContent }}</template>
+									<MkInput v-model="element.value" small :placeholder="i18n.ts._profile.metadataContent">
 									</MkInput>
 								</FormSplit>
 							</div>
 						</div>
 					</template>
 				</Sortable>
-
-				<MkInfo>{{ i18n.ts._profile.verifiedLinkDescription }}</MkInfo>
 			</div>
 		</MkFolder>
 		<template #caption>{{ i18n.ts._profile.metadataDescription }}</template>
@@ -310,19 +309,11 @@ definePageMetadata(() => ({
 	container-type: inline-size;
 }
 
-.metadataMargin {
-	margin-bottom: 1.5em;
-}
-
 .fieldDragItem {
 	display: flex;
-	padding-bottom: .75em;
+	padding: 10px;
 	align-items: flex-end;
-	border-bottom: solid 0.5px var(--divider);
-
-	&:last-child {
-		border-bottom: 0;
-	}
+	border-radius: 6px;
 
 	/* (drag button) 32px + (drag button margin) 8px + (input width) 200px * 2 + (input gap) 12px = 452px */
 	@container (max-width: 452px) {

From ae3c155490d9b5a574c45309744ba2a0cbe78932 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=8B=E3=81=A3=E3=81=93=E3=81=8B=E3=82=8A?=
 <67428053+kakkokari-gtyih@users.noreply.github.com>
Date: Sat, 5 Oct 2024 12:03:47 +0900
Subject: [PATCH 042/121] =?UTF-8?q?fix:=20signin=20=E3=81=AE=E8=B3=87?=
 =?UTF-8?q?=E6=A0=BC=E6=83=85=E5=A0=B1=E3=81=8C=E8=B6=B3=E3=82=8A=E3=81=AA?=
 =?UTF-8?q?=E3=81=84=E3=81=A0=E3=81=91=E3=81=AE=E5=A0=B4=E5=90=88=E3=81=AF?=
 =?UTF-8?q?=E3=82=A8=E3=83=A9=E3=83=BC=E3=81=AB=E3=81=9B=E3=81=9A200?=
 =?UTF-8?q?=E3=82=92=E8=BF=94=E3=81=99=E3=82=88=E3=81=86=E3=81=AB=20(#1470?=
 =?UTF-8?q?0)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

* fix: signin の資格情報が足りないだけの場合はエラーにせず200を返すように

* run api extractor

* fix

* fix

* fix test

* /signin -> /signin-flow

* fix

* fix lint

* rename

* fix

* fix
---
 cypress/e2e/basic.cy.ts                       |   2 +-
 cypress/support/commands.ts                   |   2 +-
 .../src/server/api/ApiServerService.ts        |   2 +-
 .../src/server/api/SigninApiService.ts        |  66 ++---
 .../backend/src/server/api/SigninService.ts   |   6 +-
 packages/backend/test/e2e/2fa.ts              |  71 +++---
 packages/backend/test/e2e/endpoints.ts        |   8 +-
 packages/frontend/src/components/MkSignin.vue | 236 +++++++++---------
 .../src/components/MkSignupDialog.form.vue    |  11 +-
 .../src/components/MkSignupDialog.vue         |   4 +-
 packages/misskey-js/etc/misskey-js.api.md     |  24 +-
 packages/misskey-js/src/api.types.ts          |  10 +-
 packages/misskey-js/src/entities.ts           |  22 +-
 13 files changed, 230 insertions(+), 234 deletions(-)

diff --git a/cypress/e2e/basic.cy.ts b/cypress/e2e/basic.cy.ts
index c9d7e0a24a..d2efbf709c 100644
--- a/cypress/e2e/basic.cy.ts
+++ b/cypress/e2e/basic.cy.ts
@@ -120,7 +120,7 @@ describe('After user signup', () => {
 	it('signin', () => {
 		cy.visitHome();
 
-		cy.intercept('POST', '/api/signin').as('signin');
+		cy.intercept('POST', '/api/signin-flow').as('signin');
 
 		cy.get('[data-cy-signin]').click();
 
diff --git a/cypress/support/commands.ts b/cypress/support/commands.ts
index ed5cda31b0..197ff963ac 100644
--- a/cypress/support/commands.ts
+++ b/cypress/support/commands.ts
@@ -55,7 +55,7 @@ Cypress.Commands.add('registerUser', (username, password, isAdmin = false) => {
 Cypress.Commands.add('login', (username, password) => {
 	cy.visitHome();
 
-	cy.intercept('POST', '/api/signin').as('signin');
+	cy.intercept('POST', '/api/signin-flow').as('signin');
 
 	cy.get('[data-cy-signin]').click();
 	cy.get('[data-cy-signin-page-input]').should('be.visible', { timeout: 1000 });
diff --git a/packages/backend/src/server/api/ApiServerService.ts b/packages/backend/src/server/api/ApiServerService.ts
index 356e145681..6b760c258b 100644
--- a/packages/backend/src/server/api/ApiServerService.ts
+++ b/packages/backend/src/server/api/ApiServerService.ts
@@ -133,7 +133,7 @@ export class ApiServerService {
 				'turnstile-response'?: string;
 				'm-captcha-response'?: string;
 			};
-		}>('/signin', (request, reply) => this.signinApiService.signin(request, reply));
+		}>('/signin-flow', (request, reply) => this.signinApiService.signin(request, reply));
 
 		fastify.post<{
 			Body: {
diff --git a/packages/backend/src/server/api/SigninApiService.ts b/packages/backend/src/server/api/SigninApiService.ts
index 81684beb3c..0d24ffa56a 100644
--- a/packages/backend/src/server/api/SigninApiService.ts
+++ b/packages/backend/src/server/api/SigninApiService.ts
@@ -5,8 +5,8 @@
 
 import { Inject, Injectable } from '@nestjs/common';
 import bcrypt from 'bcryptjs';
-import * as OTPAuth from 'otpauth';
 import { IsNull } from 'typeorm';
+import * as Misskey from 'misskey-js';
 import { DI } from '@/di-symbols.js';
 import type {
 	MiMeta,
@@ -26,27 +26,9 @@ import { CaptchaService } from '@/core/CaptchaService.js';
 import { FastifyReplyError } from '@/misc/fastify-reply-error.js';
 import { RateLimiterService } from './RateLimiterService.js';
 import { SigninService } from './SigninService.js';
-import type { AuthenticationResponseJSON, PublicKeyCredentialRequestOptionsJSON } from '@simplewebauthn/types';
+import type { AuthenticationResponseJSON } from '@simplewebauthn/types';
 import type { FastifyReply, FastifyRequest } from 'fastify';
 
-/**
- * next を指定すると、次にクライアント側で行うべき処理を指定できる。
- *
- * - `captcha`: パスワードと、(有効になっている場合は)CAPTCHAを求める
- * - `password`: パスワードを求める
- * - `totp`: ワンタイムパスワードを求める
- * - `passkey`: WebAuthn認証を求める(WebAuthnに対応していないブラウザの場合はワンタイムパスワード)
- */
-
-type SigninErrorResponse = {
-	id: string;
-	next?: 'captcha' | 'password' | 'totp';
-} | {
-	id: string;
-	next: 'passkey';
-	authRequest: PublicKeyCredentialRequestOptionsJSON;
-};
-
 @Injectable()
 export class SigninApiService {
 	constructor(
@@ -101,7 +83,7 @@ export class SigninApiService {
 		const password = body['password'];
 		const token = body['token'];
 
-		function error(status: number, error: SigninErrorResponse) {
+		function error(status: number, error: { id: string }) {
 			reply.code(status);
 			return { error };
 		}
@@ -152,21 +134,17 @@ export class SigninApiService {
 		const securityKeysAvailable = await this.userSecurityKeysRepository.countBy({ userId: user.id }).then(result => result >= 1);
 
 		if (password == null) {
-			reply.code(403);
+			reply.code(200);
 			if (profile.twoFactorEnabled) {
 				return {
-					error: {
-						id: '144ff4f8-bd6c-41bc-82c3-b672eb09efbf',
-						next: 'password',
-					},
-				} satisfies { error: SigninErrorResponse };
+					finished: false,
+					next: 'password',
+				} satisfies Misskey.entities.SigninFlowResponse;
 			} else {
 				return {
-					error: {
-						id: '144ff4f8-bd6c-41bc-82c3-b672eb09efbf',
-						next: 'captcha',
-					},
-				} satisfies { error: SigninErrorResponse };
+					finished: false,
+					next: 'captcha',
+				} satisfies Misskey.entities.SigninFlowResponse;
 			}
 		}
 
@@ -178,7 +156,7 @@ export class SigninApiService {
 		// Compare password
 		const same = await bcrypt.compare(password, profile.password!);
 
-		const fail = async (status?: number, failure?: SigninErrorResponse) => {
+		const fail = async (status?: number, failure?: { id: string; }) => {
 			// Append signin history
 			await this.signinsRepository.insert({
 				id: this.idService.gen(),
@@ -268,27 +246,23 @@ export class SigninApiService {
 
 			const authRequest = await this.webAuthnService.initiateAuthentication(user.id);
 
-			reply.code(403);
+			reply.code(200);
 			return {
-				error: {
-					id: '06e661b9-8146-4ae3-bde5-47138c0ae0c4',
-					next: 'passkey',
-					authRequest,
-				},
-			} satisfies { error: SigninErrorResponse };
+				finished: false,
+				next: 'passkey',
+				authRequest,
+			} satisfies Misskey.entities.SigninFlowResponse;
 		} else {
 			if (!same || !profile.twoFactorEnabled) {
 				return await fail(403, {
 					id: '932c904e-9460-45b7-9ce6-7ed33be7eb2c',
 				});
 			} else {
-				reply.code(403);
+				reply.code(200);
 				return {
-					error: {
-						id: '144ff4f8-bd6c-41bc-82c3-b672eb09efbf',
-						next: 'totp',
-					},
-				} satisfies { error: SigninErrorResponse };
+					finished: false,
+					next: 'totp',
+				} satisfies Misskey.entities.SigninFlowResponse;
 			}
 		}
 		// never get here
diff --git a/packages/backend/src/server/api/SigninService.ts b/packages/backend/src/server/api/SigninService.ts
index 4b041f373f..640356b50c 100644
--- a/packages/backend/src/server/api/SigninService.ts
+++ b/packages/backend/src/server/api/SigninService.ts
@@ -4,6 +4,7 @@
  */
 
 import { Inject, Injectable } from '@nestjs/common';
+import * as Misskey from 'misskey-js';
 import { DI } from '@/di-symbols.js';
 import type { SigninsRepository, UserProfilesRepository } from '@/models/_.js';
 import { IdService } from '@/core/IdService.js';
@@ -57,9 +58,10 @@ export class SigninService {
 
 		reply.code(200);
 		return {
+			finished: true,
 			id: user.id,
-			i: user.token,
-		};
+			i: user.token!,
+		} satisfies Misskey.entities.SigninFlowResponse;
 	}
 }
 
diff --git a/packages/backend/test/e2e/2fa.ts b/packages/backend/test/e2e/2fa.ts
index 88c32b4346..48e1bababb 100644
--- a/packages/backend/test/e2e/2fa.ts
+++ b/packages/backend/test/e2e/2fa.ts
@@ -136,7 +136,7 @@ describe('2要素認証', () => {
 		keyName: string,
 		credentialId: Buffer,
 		requestOptions: PublicKeyCredentialRequestOptionsJSON,
-	}): misskey.entities.SigninRequest => {
+	}): misskey.entities.SigninFlowRequest => {
 		// AuthenticatorAssertionResponse.authenticatorData
 		// https://developer.mozilla.org/en-US/docs/Web/API/AuthenticatorAssertionResponse/authenticatorData
 		const authenticatorData = Buffer.concat([
@@ -196,22 +196,21 @@ describe('2要素認証', () => {
 		}, alice);
 		assert.strictEqual(doneResponse.status, 200);
 
-		const signinWithoutTokenResponse = await api('signin', {
+		const signinWithoutTokenResponse = await api('signin-flow', {
 			...signinParam(),
 		});
-		assert.strictEqual(signinWithoutTokenResponse.status, 403);
+		assert.strictEqual(signinWithoutTokenResponse.status, 200);
 		assert.deepStrictEqual(signinWithoutTokenResponse.body, {
-			error: {
-				id: '144ff4f8-bd6c-41bc-82c3-b672eb09efbf',
-				next: 'totp',
-			},
+			finished: false,
+			next: 'totp',
 		});
 
-		const signinResponse = await api('signin', {
+		const signinResponse = await api('signin-flow', {
 			...signinParam(),
 			token: otpToken(registerResponse.body.secret),
 		});
 		assert.strictEqual(signinResponse.status, 200);
+		assert.strictEqual(signinResponse.body.finished, true);
 		assert.notEqual(signinResponse.body.i, undefined);
 
 		// 後片付け
@@ -252,29 +251,23 @@ describe('2要素認証', () => {
 		assert.strictEqual(keyDoneResponse.body.id, credentialId.toString('base64url'));
 		assert.strictEqual(keyDoneResponse.body.name, keyName);
 
-		const signinResponse = await api('signin', {
+		const signinResponse = await api('signin-flow', {
 			...signinParam(),
 		});
-		const signinResponseBody = signinResponse.body as unknown as {
-			error: {
-				id: string;
-				next: 'passkey';
-				authRequest: PublicKeyCredentialRequestOptionsJSON;
-			};
-		};
-		assert.strictEqual(signinResponse.status, 403);
-		assert.strictEqual(signinResponseBody.error.id, '06e661b9-8146-4ae3-bde5-47138c0ae0c4');
-		assert.strictEqual(signinResponseBody.error.next, 'passkey');
-		assert.notEqual(signinResponseBody.error.authRequest.challenge, undefined);
-		assert.notEqual(signinResponseBody.error.authRequest.allowCredentials, undefined);
-		assert.strictEqual(signinResponseBody.error.authRequest.allowCredentials && signinResponseBody.error.authRequest.allowCredentials[0]?.id, credentialId.toString('base64url'));
+		assert.strictEqual(signinResponse.status, 200);
+		assert.strictEqual(signinResponse.body.finished, false);
+		assert.strictEqual(signinResponse.body.next, 'passkey');
+		assert.notEqual(signinResponse.body.authRequest.challenge, undefined);
+		assert.notEqual(signinResponse.body.authRequest.allowCredentials, undefined);
+		assert.strictEqual(signinResponse.body.authRequest.allowCredentials && signinResponse.body.authRequest.allowCredentials[0]?.id, credentialId.toString('base64url'));
 
-		const signinResponse2 = await api('signin', signinWithSecurityKeyParam({
+		const signinResponse2 = await api('signin-flow', signinWithSecurityKeyParam({
 			keyName,
 			credentialId,
-			requestOptions: signinResponseBody.error.authRequest,
+			requestOptions: signinResponse.body.authRequest,
 		}));
 		assert.strictEqual(signinResponse2.status, 200);
+		assert.strictEqual(signinResponse2.body.finished, true);
 		assert.notEqual(signinResponse2.body.i, undefined);
 
 		// 後片付け
@@ -320,32 +313,26 @@ describe('2要素認証', () => {
 		assert.strictEqual(iResponse.status, 200);
 		assert.strictEqual(iResponse.body.usePasswordLessLogin, true);
 
-		const signinResponse = await api('signin', {
+		const signinResponse = await api('signin-flow', {
 			...signinParam(),
 			password: '',
 		});
-		const signinResponseBody = signinResponse.body as unknown as {
-			error: {
-				id: string;
-				next: 'passkey';
-				authRequest: PublicKeyCredentialRequestOptionsJSON;
-			};
-		};
-		assert.strictEqual(signinResponse.status, 403);
-		assert.strictEqual(signinResponseBody.error.id, '06e661b9-8146-4ae3-bde5-47138c0ae0c4');
-		assert.strictEqual(signinResponseBody.error.next, 'passkey');
-		assert.notEqual(signinResponseBody.error.authRequest.challenge, undefined);
-		assert.notEqual(signinResponseBody.error.authRequest.allowCredentials, undefined);
+		assert.strictEqual(signinResponse.status, 200);
+		assert.strictEqual(signinResponse.body.finished, false);
+		assert.strictEqual(signinResponse.body.next, 'passkey');
+		assert.notEqual(signinResponse.body.authRequest.challenge, undefined);
+		assert.notEqual(signinResponse.body.authRequest.allowCredentials, undefined);
 
-		const signinResponse2 = await api('signin', {
+		const signinResponse2 = await api('signin-flow', {
 			...signinWithSecurityKeyParam({
 				keyName,
 				credentialId,
-				requestOptions: signinResponseBody.error.authRequest,
+				requestOptions: signinResponse.body.authRequest,
 			} as any),
 			password: '',
 		});
 		assert.strictEqual(signinResponse2.status, 200);
+		assert.strictEqual(signinResponse2.body.finished, true);
 		assert.notEqual(signinResponse2.body.i, undefined);
 
 		// 後片付け
@@ -450,11 +437,12 @@ describe('2要素認証', () => {
 		assert.strictEqual(afterIResponse.status, 200);
 		assert.strictEqual(afterIResponse.body.securityKeys, false);
 
-		const signinResponse = await api('signin', {
+		const signinResponse = await api('signin-flow', {
 			...signinParam(),
 			token: otpToken(registerResponse.body.secret),
 		});
 		assert.strictEqual(signinResponse.status, 200);
+		assert.strictEqual(signinResponse.body.finished, true);
 		assert.notEqual(signinResponse.body.i, undefined);
 
 		// 後片付け
@@ -485,10 +473,11 @@ describe('2要素認証', () => {
 		}, alice);
 		assert.strictEqual(unregisterResponse.status, 204);
 
-		const signinResponse = await api('signin', {
+		const signinResponse = await api('signin-flow', {
 			...signinParam(),
 		});
 		assert.strictEqual(signinResponse.status, 200);
+		assert.strictEqual(signinResponse.body.finished, true);
 		assert.notEqual(signinResponse.body.i, undefined);
 
 		// 後片付け
diff --git a/packages/backend/test/e2e/endpoints.ts b/packages/backend/test/e2e/endpoints.ts
index 5aaec7f6f9..b91d77c398 100644
--- a/packages/backend/test/e2e/endpoints.ts
+++ b/packages/backend/test/e2e/endpoints.ts
@@ -66,9 +66,9 @@ describe('Endpoints', () => {
 		});
 	});
 
-	describe('signin', () => {
+	describe('signin-flow', () => {
 		test('間違ったパスワードでサインインできない', async () => {
-			const res = await api('signin', {
+			const res = await api('signin-flow', {
 				username: 'test1',
 				password: 'bar',
 			});
@@ -77,7 +77,7 @@ describe('Endpoints', () => {
 		});
 
 		test('クエリをインジェクションできない', async () => {
-			const res = await api('signin', {
+			const res = await api('signin-flow', {
 				username: 'test1',
 				// @ts-expect-error password must be string
 				password: {
@@ -89,7 +89,7 @@ describe('Endpoints', () => {
 		});
 
 		test('正しい情報でサインインできる', async () => {
-			const res = await api('signin', {
+			const res = await api('signin-flow', {
 				username: 'test1',
 				password: 'test1',
 			});
diff --git a/packages/frontend/src/components/MkSignin.vue b/packages/frontend/src/components/MkSignin.vue
index 03dd61f6c6..26e1ac516c 100644
--- a/packages/frontend/src/components/MkSignin.vue
+++ b/packages/frontend/src/components/MkSignin.vue
@@ -83,7 +83,7 @@ import type { AuthenticationPublicKeyCredential } from '@github/webauthn-json/br
 import type { OpenOnRemoteOptions } from '@/scripts/please-login.js';
 
 const emit = defineEmits<{
-	(ev: 'login', v: Misskey.entities.SigninResponse): void;
+	(ev: 'login', v: Misskey.entities.SigninFlowResponse): void;
 }>();
 
 const props = withDefaults(defineProps<{
@@ -212,23 +212,63 @@ async function onTotpSubmitted(token: string) {
 	}
 }
 
-async function tryLogin(req: Partial<Misskey.entities.SigninRequest>): Promise<Misskey.entities.SigninResponse> {
+async function tryLogin(req: Partial<Misskey.entities.SigninFlowRequest>): Promise<Misskey.entities.SigninFlowResponse> {
 	const _req = {
 		username: req.username ?? userInfo.value?.username,
 		...req,
 	};
 
-	function assertIsSigninRequest(x: Partial<Misskey.entities.SigninRequest>): x is Misskey.entities.SigninRequest {
+	function assertIsSigninFlowRequest(x: Partial<Misskey.entities.SigninFlowRequest>): x is Misskey.entities.SigninFlowRequest {
 		return x.username != null;
 	}
 
-	if (!assertIsSigninRequest(_req)) {
+	if (!assertIsSigninFlowRequest(_req)) {
 		throw new Error('Invalid request');
 	}
 
-	return await misskeyApi('signin', _req).then(async (res) => {
-		emit('login', res);
-		await onLoginSucceeded(res);
+	return await misskeyApi('signin-flow', _req).then(async (res) => {
+		if (res.finished) {
+			emit('login', res);
+			await onLoginSucceeded(res);
+		} else {
+			switch (res.next) {
+				case 'captcha': {
+					needCaptcha.value = true;
+					page.value = 'password';
+					break;
+				}
+				case 'password': {
+					needCaptcha.value = false;
+					page.value = 'password';
+					break;
+				}
+				case 'totp': {
+					page.value = 'totp';
+					break;
+				}
+				case 'passkey': {
+					if (webAuthnSupported()) {
+						credentialRequest.value = parseRequestOptionsFromJSON({
+							publicKey: res.authRequest,
+						});
+						page.value = 'passkey';
+					} else {
+						page.value = 'totp';
+					}
+					break;
+				}
+			}
+
+			if (doingPasskeyFromInputPage.value === true) {
+				doingPasskeyFromInputPage.value = false;
+				page.value = 'input';
+				password.value = '';
+			}
+			passwordPageEl.value?.resetCaptcha();
+			nextTick(() => {
+				waiting.value = false;
+			});
+		}
 		return res;
 	}).catch((err) => {
 		onSigninApiError(err);
@@ -236,7 +276,7 @@ async function tryLogin(req: Partial<Misskey.entities.SigninRequest>): Promise<M
 	});
 }
 
-async function onLoginSucceeded(res: Misskey.entities.SigninResponse) {
+async function onLoginSucceeded(res: Misskey.entities.SigninFlowResponse & { finished: true; }) {
 	if (props.autoSet) {
 		await login(res.i);
 	}
@@ -245,112 +285,82 @@ async function onLoginSucceeded(res: Misskey.entities.SigninResponse) {
 function onSigninApiError(err?: any): void {
 	const id = err?.id ?? null;
 
-	if (typeof err === 'object' && 'next' in err) {
-		switch (err.next) {
-			case 'captcha': {
-				needCaptcha.value = true;
-				page.value = 'password';
-				break;
-			}
-			case 'password': {
-				needCaptcha.value = false;
-				page.value = 'password';
-				break;
-			}
-			case 'totp': {
-				page.value = 'totp';
-				break;
-			}
-			case 'passkey': {
-				if (webAuthnSupported() && 'authRequest' in err) {
-					credentialRequest.value = parseRequestOptionsFromJSON({
-						publicKey: err.authRequest,
-					});
-					page.value = 'passkey';
-				} else {
-					page.value = 'totp';
-				}
-				break;
-			}
+	switch (id) {
+		case '6cc579cc-885d-43d8-95c2-b8c7fc963280': {
+			os.alert({
+				type: 'error',
+				title: i18n.ts.loginFailed,
+				text: i18n.ts.noSuchUser,
+			});
+			break;
 		}
-	} else {
-		switch (id) {
-			case '6cc579cc-885d-43d8-95c2-b8c7fc963280': {
-				os.alert({
-					type: 'error',
-					title: i18n.ts.loginFailed,
-					text: i18n.ts.noSuchUser,
-				});
-				break;
-			}
-			case '932c904e-9460-45b7-9ce6-7ed33be7eb2c': {
-				os.alert({
-					type: 'error',
-					title: i18n.ts.loginFailed,
-					text: i18n.ts.incorrectPassword,
-				});
-				break;
-			}
-			case 'e03a5f46-d309-4865-9b69-56282d94e1eb': {
-				showSuspendedDialog();
-				break;
-			}
-			case '22d05606-fbcf-421a-a2db-b32610dcfd1b': {
-				os.alert({
-					type: 'error',
-					title: i18n.ts.loginFailed,
-					text: i18n.ts.rateLimitExceeded,
-				});
-				break;
-			}
-			case 'cdf1235b-ac71-46d4-a3a6-84ccce48df6f': {
-				os.alert({
-					type: 'error',
-					title: i18n.ts.loginFailed,
-					text: i18n.ts.incorrectTotp,
-				});
-				break;
-			}
-			case '36b96a7d-b547-412d-aeed-2d611cdc8cdc': {
-				os.alert({
-					type: 'error',
-					title: i18n.ts.loginFailed,
-					text: i18n.ts.unknownWebAuthnKey,
-				});
-				break;
-			}
-			case '93b86c4b-72f9-40eb-9815-798928603d1e': {
-				os.alert({
-					type: 'error',
-					title: i18n.ts.loginFailed,
-					text: i18n.ts.passkeyVerificationFailed,
-				});
-				break;
-			}
-			case 'b18c89a7-5b5e-4cec-bb5b-0419f332d430': {
-				os.alert({
-					type: 'error',
-					title: i18n.ts.loginFailed,
-					text: i18n.ts.passkeyVerificationFailed,
-				});
-				break;
-			}
-			case '2d84773e-f7b7-4d0b-8f72-bb69b584c912': {
-				os.alert({
-					type: 'error',
-					title: i18n.ts.loginFailed,
-					text: i18n.ts.passkeyVerificationSucceededButPasswordlessLoginDisabled,
-				});
-				break;
-			}
-			default: {
-				console.error(err);
-				os.alert({
-					type: 'error',
-					title: i18n.ts.loginFailed,
-					text: JSON.stringify(err),
-				});
-			}
+		case '932c904e-9460-45b7-9ce6-7ed33be7eb2c': {
+			os.alert({
+				type: 'error',
+				title: i18n.ts.loginFailed,
+				text: i18n.ts.incorrectPassword,
+			});
+			break;
+		}
+		case 'e03a5f46-d309-4865-9b69-56282d94e1eb': {
+			showSuspendedDialog();
+			break;
+		}
+		case '22d05606-fbcf-421a-a2db-b32610dcfd1b': {
+			os.alert({
+				type: 'error',
+				title: i18n.ts.loginFailed,
+				text: i18n.ts.rateLimitExceeded,
+			});
+			break;
+		}
+		case 'cdf1235b-ac71-46d4-a3a6-84ccce48df6f': {
+			os.alert({
+				type: 'error',
+				title: i18n.ts.loginFailed,
+				text: i18n.ts.incorrectTotp,
+			});
+			break;
+		}
+		case '36b96a7d-b547-412d-aeed-2d611cdc8cdc': {
+			os.alert({
+				type: 'error',
+				title: i18n.ts.loginFailed,
+				text: i18n.ts.unknownWebAuthnKey,
+			});
+			break;
+		}
+		case '93b86c4b-72f9-40eb-9815-798928603d1e': {
+			os.alert({
+				type: 'error',
+				title: i18n.ts.loginFailed,
+				text: i18n.ts.passkeyVerificationFailed,
+			});
+			break;
+		}
+		case 'b18c89a7-5b5e-4cec-bb5b-0419f332d430': {
+			os.alert({
+				type: 'error',
+				title: i18n.ts.loginFailed,
+				text: i18n.ts.passkeyVerificationFailed,
+			});
+			break;
+		}
+		case '2d84773e-f7b7-4d0b-8f72-bb69b584c912': {
+			os.alert({
+				type: 'error',
+				title: i18n.ts.loginFailed,
+				text: i18n.ts.passkeyVerificationSucceededButPasswordlessLoginDisabled,
+			});
+			break;
+		}
+		default: {
+			console.error(err);
+			os.alert({
+				type: 'error',
+				title: i18n.ts.loginFailed,
+				text: JSON.stringify(err),
+			});
 		}
 	}
 
diff --git a/packages/frontend/src/components/MkSignupDialog.form.vue b/packages/frontend/src/components/MkSignupDialog.form.vue
index 38cac7f644..ff096dc729 100644
--- a/packages/frontend/src/components/MkSignupDialog.form.vue
+++ b/packages/frontend/src/components/MkSignupDialog.form.vue
@@ -98,7 +98,7 @@ const props = withDefaults(defineProps<{
 });
 
 const emit = defineEmits<{
-	(ev: 'signup', user: Misskey.entities.SigninResponse): void;
+	(ev: 'signup', user: Misskey.entities.SigninFlowResponse): void;
 	(ev: 'signupEmailPending'): void;
 }>();
 
@@ -269,14 +269,19 @@ async function onSubmit(): Promise<void> {
 			});
 			emit('signupEmailPending');
 		} else {
-			const res = await misskeyApi('signin', {
+			const res = await misskeyApi('signin-flow', {
 				username: username.value,
 				password: password.value,
 			});
 			emit('signup', res);
 
-			if (props.autoSet) {
+			if (props.autoSet && res.finished) {
 				return login(res.i);
+			} else {
+				os.alert({
+					type: 'error',
+					text: i18n.ts.somethingHappened,
+				});
 			}
 		}
 	} catch {
diff --git a/packages/frontend/src/components/MkSignupDialog.vue b/packages/frontend/src/components/MkSignupDialog.vue
index 97310d32a6..4cccd99492 100644
--- a/packages/frontend/src/components/MkSignupDialog.vue
+++ b/packages/frontend/src/components/MkSignupDialog.vue
@@ -47,7 +47,7 @@ const props = withDefaults(defineProps<{
 });
 
 const emit = defineEmits<{
-	(ev: 'done', res: Misskey.entities.SigninResponse): void;
+	(ev: 'done', res: Misskey.entities.SigninFlowResponse): void;
 	(ev: 'closed'): void;
 }>();
 
@@ -55,7 +55,7 @@ const dialog = shallowRef<InstanceType<typeof MkModalWindow>>();
 
 const isAcceptedServerRule = ref(false);
 
-function onSignup(res: Misskey.entities.SigninResponse) {
+function onSignup(res: Misskey.entities.SigninFlowResponse) {
 	emit('done', res);
 	dialog.value?.close();
 }
diff --git a/packages/misskey-js/etc/misskey-js.api.md b/packages/misskey-js/etc/misskey-js.api.md
index 9ad784c296..732352abd8 100644
--- a/packages/misskey-js/etc/misskey-js.api.md
+++ b/packages/misskey-js/etc/misskey-js.api.md
@@ -1158,9 +1158,9 @@ export type Endpoints = Overwrite<Endpoints_2, {
         req: SignupPendingRequest;
         res: SignupPendingResponse;
     };
-    'signin': {
-        req: SigninRequest;
-        res: SigninResponse;
+    'signin-flow': {
+        req: SigninFlowRequest;
+        res: SigninFlowResponse;
     };
     'signin-with-passkey': {
         req: SigninWithPasskeyRequest;
@@ -1208,11 +1208,11 @@ declare namespace entities {
         SignupResponse,
         SignupPendingRequest,
         SignupPendingResponse,
-        SigninRequest,
+        SigninFlowRequest,
+        SigninFlowResponse,
         SigninWithPasskeyRequest,
         SigninWithPasskeyInitResponse,
         SigninWithPasskeyResponse,
-        SigninResponse,
         PartialRolePolicyOverride,
         EmptyRequest,
         EmptyResponse,
@@ -3038,7 +3038,7 @@ type ServerStatsLog = ServerStats[];
 type Signin = components['schemas']['Signin'];
 
 // @public (undocumented)
-type SigninRequest = {
+type SigninFlowRequest = {
     username: string;
     password?: string;
     token?: string;
@@ -3050,9 +3050,17 @@ type SigninRequest = {
 };
 
 // @public (undocumented)
-type SigninResponse = {
+type SigninFlowResponse = {
+    finished: true;
     id: User['id'];
     i: string;
+} | {
+    finished: false;
+    next: 'captcha' | 'password' | 'totp';
+} | {
+    finished: false;
+    next: 'passkey';
+    authRequest: PublicKeyCredentialRequestOptionsJSON;
 };
 
 // @public (undocumented)
@@ -3069,7 +3077,7 @@ type SigninWithPasskeyRequest = {
 
 // @public (undocumented)
 type SigninWithPasskeyResponse = {
-    signinResponse: SigninResponse;
+    signinResponse: SigninFlowResponse;
 };
 
 // @public (undocumented)
diff --git a/packages/misskey-js/src/api.types.ts b/packages/misskey-js/src/api.types.ts
index cef5ab8861..838949f8e1 100644
--- a/packages/misskey-js/src/api.types.ts
+++ b/packages/misskey-js/src/api.types.ts
@@ -3,8 +3,8 @@ import { UserDetailed } from './autogen/models.js';
 import { AdminRolesCreateRequest, AdminRolesCreateResponse, UsersShowRequest } from './autogen/entities.js';
 import {
 	PartialRolePolicyOverride,
-	SigninRequest,
-	SigninResponse,
+	SigninFlowRequest,
+	SigninFlowResponse,
 	SigninWithPasskeyInitResponse,
 	SigninWithPasskeyRequest,
 	SigninWithPasskeyResponse,
@@ -81,9 +81,9 @@ export type Endpoints = Overwrite<
 			res: SignupPendingResponse;
 		},
 		// api.jsonには載せないものなのでここで定義
-		'signin': {
-			req: SigninRequest;
-			res: SigninResponse;
+		'signin-flow': {
+			req: SigninFlowRequest;
+			res: SigninFlowResponse;
 		},
 		'signin-with-passkey': {
 			req: SigninWithPasskeyRequest;
diff --git a/packages/misskey-js/src/entities.ts b/packages/misskey-js/src/entities.ts
index 98ac50e5a1..8bbc9c113b 100644
--- a/packages/misskey-js/src/entities.ts
+++ b/packages/misskey-js/src/entities.ts
@@ -267,7 +267,7 @@ export type SignupPendingResponse = {
 	i: string,
 };
 
-export type SigninRequest = {
+export type SigninFlowRequest = {
 	username: string;
 	password?: string;
 	token?: string;
@@ -278,6 +278,19 @@ export type SigninRequest = {
 	'm-captcha-response'?: string | null;
 };
 
+export type SigninFlowResponse = {
+	finished: true;
+	id: User['id'];
+	i: string;
+} | {
+	finished: false;
+	next: 'captcha' | 'password' | 'totp';
+} | {
+	finished: false;
+	next: 'passkey';
+	authRequest: PublicKeyCredentialRequestOptionsJSON;
+};
+
 export type SigninWithPasskeyRequest = {
 	credential?: AuthenticationResponseJSON;
 	context?: string;
@@ -289,12 +302,7 @@ export type SigninWithPasskeyInitResponse = {
 };
 
 export type SigninWithPasskeyResponse = {
-	signinResponse: SigninResponse;
-};
-
-export type SigninResponse = {
-	id: User['id'],
-	i: string,
+	signinResponse: SigninFlowResponse;
 };
 
 type Values<T extends Record<PropertyKey, unknown>> = T[keyof T];

From 88698462a91e0fe15501a44f923a812d169bb030 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: Sat, 5 Oct 2024 12:51:46 +0900
Subject: [PATCH 043/121] =?UTF-8?q?feat(backend):=20=E9=80=9A=E5=A0=B1?=
 =?UTF-8?q?=E3=81=8A=E3=82=88=E3=81=B3=E9=80=9A=E5=A0=B1=E8=A7=A3=E6=B1=BA?=
 =?UTF-8?q?=E6=99=82=E3=81=AB=E9=80=81=E5=87=BA=E3=81=95=E3=82=8C=E3=82=8B?=
 =?UTF-8?q?SystemWebhook=E3=81=AB=E3=83=A6=E3=83=BC=E3=82=B6=E6=83=85?=
 =?UTF-8?q?=E5=A0=B1=E3=82=92=E5=90=AB=E3=82=81=E3=82=8B=E3=82=88=E3=81=86?=
 =?UTF-8?q?=E3=81=AB=E3=81=99=E3=82=8B=20(#14698)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

* feat(backend): 通報および通報解決時に送出されるSystemWebhookにユーザ情報を含めるようにする

* テスト送信もペイロード形式を合わせる

* add spaces

* fix test
---
 CHANGELOG.md                                  |  2 +-
 .../core/AbuseReportNotificationService.ts    | 24 ++++++++++++++++++-
 .../backend/src/core/WebhookTestService.ts    | 20 +++++++++++++---
 .../unit/AbuseReportNotificationService.ts    |  6 ++++-
 4 files changed, 46 insertions(+), 6 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index a31be063f0..04acc11ac3 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -21,7 +21,7 @@
 ### Server
 - Enhance: セキュリティ向上のため、ログイン時にメール通知を行うように
 - Enhance: 自分とモデレーター以外のユーザーから二要素認証関連のデータが取得できないように
-
+- Enhance: 通報および通報解決時に送出されるSystemWebhookにユーザ情報を含めるように ( #14697 )
 
 ## 2024.9.0
 
diff --git a/packages/backend/src/core/AbuseReportNotificationService.ts b/packages/backend/src/core/AbuseReportNotificationService.ts
index fe2c63e7d6..fb7c7bd2c3 100644
--- a/packages/backend/src/core/AbuseReportNotificationService.ts
+++ b/packages/backend/src/core/AbuseReportNotificationService.ts
@@ -22,6 +22,7 @@ import { RoleService } from '@/core/RoleService.js';
 import { RecipientMethod } from '@/models/AbuseReportNotificationRecipient.js';
 import { ModerationLogService } from '@/core/ModerationLogService.js';
 import { SystemWebhookService } from '@/core/SystemWebhookService.js';
+import { UserEntityService } from '@/core/entities/UserEntityService.js';
 import { IdService } from './IdService.js';
 
 @Injectable()
@@ -42,6 +43,7 @@ export class AbuseReportNotificationService implements OnApplicationShutdown {
 		private emailService: EmailService,
 		private moderationLogService: ModerationLogService,
 		private globalEventService: GlobalEventService,
+		private userEntityService: UserEntityService,
 	) {
 		this.redisForSub.on('message', this.onMessage);
 	}
@@ -135,6 +137,26 @@ export class AbuseReportNotificationService implements OnApplicationShutdown {
 			return;
 		}
 
+		const usersMap = await this.userEntityService.packMany(
+			[
+				...new Set([
+					...abuseReports.map(it => it.reporter ?? it.reporterId),
+					...abuseReports.map(it => it.targetUser ?? it.targetUserId),
+					...abuseReports.map(it => it.assignee ?? it.assigneeId),
+				].filter(x => x != null)),
+			],
+			null,
+			{ schema: 'UserLite' },
+		).then(it => new Map(it.map(it => [it.id, it])));
+		const convertedReports = abuseReports.map(it => {
+			return {
+				...it,
+				reporter: usersMap.get(it.reporterId),
+				targetUser: usersMap.get(it.targetUserId),
+				assignee: it.assigneeId ? usersMap.get(it.assigneeId) : null,
+			};
+		});
+
 		const recipientWebhookIds = await this.fetchWebhookRecipients()
 			.then(it => it
 				.filter(it => it.isActive && it.systemWebhookId && it.method === 'webhook')
@@ -142,7 +164,7 @@ export class AbuseReportNotificationService implements OnApplicationShutdown {
 				.filter(x => x != null));
 		for (const webhookId of recipientWebhookIds) {
 			await Promise.all(
-				abuseReports.map(it => {
+				convertedReports.map(it => {
 					return this.systemWebhookService.enqueueSystemWebhook(
 						webhookId,
 						type,
diff --git a/packages/backend/src/core/WebhookTestService.ts b/packages/backend/src/core/WebhookTestService.ts
index c2764f30e8..149c753d4c 100644
--- a/packages/backend/src/core/WebhookTestService.ts
+++ b/packages/backend/src/core/WebhookTestService.ts
@@ -15,8 +15,14 @@ import { QueueService } from '@/core/QueueService.js';
 
 const oneDayMillis = 24 * 60 * 60 * 1000;
 
-function generateAbuseReport(override?: Partial<MiAbuseUserReport>): MiAbuseUserReport {
-	return {
+type AbuseUserReportDto = Omit<MiAbuseUserReport, 'targetUser' | 'reporter' | 'assignee'> & {
+	targetUser: Packed<'UserLite'> | null,
+	reporter: Packed<'UserLite'> | null,
+	assignee: Packed<'UserLite'> | null,
+};
+
+function generateAbuseReport(override?: Partial<MiAbuseUserReport>): AbuseUserReportDto {
+	const result: MiAbuseUserReport = {
 		id: 'dummy-abuse-report1',
 		targetUserId: 'dummy-target-user',
 		targetUser: null,
@@ -31,6 +37,13 @@ function generateAbuseReport(override?: Partial<MiAbuseUserReport>): MiAbuseUser
 		reporterHost: null,
 		...override,
 	};
+
+	return {
+		...result,
+		targetUser: result.targetUser ? toPackedUserLite(result.targetUser) : null,
+		reporter: result.reporter ? toPackedUserLite(result.reporter) : null,
+		assignee: result.assignee ? toPackedUserLite(result.assignee) : null,
+	};
 }
 
 function generateDummyUser(override?: Partial<MiUser>): MiUser {
@@ -268,7 +281,8 @@ const dummyUser3 = generateDummyUser({
 
 @Injectable()
 export class WebhookTestService {
-	public static NoSuchWebhookError = class extends Error {};
+	public static NoSuchWebhookError = class extends Error {
+	};
 
 	constructor(
 		private userWebhookService: UserWebhookService,
diff --git a/packages/backend/test/unit/AbuseReportNotificationService.ts b/packages/backend/test/unit/AbuseReportNotificationService.ts
index e971659070..235af29f0d 100644
--- a/packages/backend/test/unit/AbuseReportNotificationService.ts
+++ b/packages/backend/test/unit/AbuseReportNotificationService.ts
@@ -5,6 +5,7 @@
 
 import { jest } from '@jest/globals';
 import { Test, TestingModule } from '@nestjs/testing';
+import { randomString } from '../utils.js';
 import { AbuseReportNotificationService } from '@/core/AbuseReportNotificationService.js';
 import {
 	AbuseReportNotificationRecipientRepository,
@@ -25,7 +26,7 @@ import { ModerationLogService } from '@/core/ModerationLogService.js';
 import { GlobalEventService } from '@/core/GlobalEventService.js';
 import { RecipientMethod } from '@/models/AbuseReportNotificationRecipient.js';
 import { SystemWebhookService } from '@/core/SystemWebhookService.js';
-import { randomString } from '../utils.js';
+import { UserEntityService } from '@/core/entities/UserEntityService.js';
 
 process.env.NODE_ENV = 'test';
 
@@ -110,6 +111,9 @@ describe('AbuseReportNotificationService', () => {
 					{
 						provide: SystemWebhookService, useFactory: () => ({ enqueueSystemWebhook: jest.fn() }),
 					},
+					{
+						provide: UserEntityService, useFactory: () => ({ pack: (v: any) => v }),
+					},
 					{
 						provide: EmailService, useFactory: () => ({ sendEmail: jest.fn() }),
 					},

From d8bf1ff7e9ab4d39b2e924bf7eae010e9b9e21f0 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=8B=E3=81=A3=E3=81=93=E3=81=8B=E3=82=8A?=
 <67428053+kakkokari-gtyih@users.noreply.github.com>
Date: Sat, 5 Oct 2024 13:47:50 +0900
Subject: [PATCH 044/121] =?UTF-8?q?#14675=20=E3=83=AC=E3=83=93=E3=83=A5?=
 =?UTF-8?q?=E3=83=BC=E3=81=AE=E4=BF=AE=E6=AD=A3=20(#14705)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 packages/backend/src/server/api/ApiServerService.ts | 2 +-
 packages/frontend/src/components/MkFukidashi.vue    | 6 +++---
 2 files changed, 4 insertions(+), 4 deletions(-)

diff --git a/packages/backend/src/server/api/ApiServerService.ts b/packages/backend/src/server/api/ApiServerService.ts
index 6b760c258b..be63635efe 100644
--- a/packages/backend/src/server/api/ApiServerService.ts
+++ b/packages/backend/src/server/api/ApiServerService.ts
@@ -125,7 +125,7 @@ export class ApiServerService {
 		fastify.post<{
 			Body: {
 				username: string;
-				password: string;
+				password?: string;
 				token?: string;
 				credential?: AuthenticationResponseJSON;
 				'hcaptcha-response'?: string;
diff --git a/packages/frontend/src/components/MkFukidashi.vue b/packages/frontend/src/components/MkFukidashi.vue
index ba82eb442f..09825487bf 100644
--- a/packages/frontend/src/components/MkFukidashi.vue
+++ b/packages/frontend/src/components/MkFukidashi.vue
@@ -8,7 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 	:class="[
 		$style.root,
 		tail === 'left' ? $style.left : $style.right,
-		negativeMargin === true && $style.negativeMergin,
+		negativeMargin === true && $style.negativeMargin,
 		shadow === true && $style.shadow,
 	]"
 >
@@ -54,7 +54,7 @@ withDefaults(defineProps<{
 	&.left {
 		padding-left: calc(var(--fukidashi-radius) * .13);
 
-		&.negativeMergin {
+		&.negativeMargin {
 			margin-left: calc(calc(var(--fukidashi-radius) * .13) * -1);
 		}
 	}
@@ -62,7 +62,7 @@ withDefaults(defineProps<{
 	&.right {
 		padding-right: calc(var(--fukidashi-radius) * .13);
 
-		&.negativeMergin {
+		&.negativeMargin {
 			margin-right: calc(calc(var(--fukidashi-radius) * .13) * -1);
 		}
 	}

From 0d7d1091c8970d9979e8efb02f0accd6dcd39422 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: Sat, 5 Oct 2024 14:37:52 +0900
Subject: [PATCH 045/121] =?UTF-8?q?enhance:=20=E4=BA=BA=E6=B0=97=E3=81=AEP?=
 =?UTF-8?q?lay=E3=82=9210=E4=BB=B6=E4=BB=A5=E4=B8=8A=E8=A1=A8=E7=A4=BA?=
 =?UTF-8?q?=E3=81=A7=E3=81=8D=E3=82=8B=E3=82=88=E3=81=86=E3=81=AB=20(#1444?=
 =?UTF-8?q?3)?=
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>
---
 CHANGELOG.md                                  |   1 +
 packages/backend/src/core/CoreModule.ts       |   5 +
 packages/backend/src/core/FlashService.ts     |  40 +++++
 .../src/core/entities/FlashEntityService.ts   |  41 +++--
 packages/backend/src/models/Flash.ts          |   5 +-
 .../server/api/endpoints/flash/featured.ts    |  22 +--
 packages/backend/test/unit/FlashService.ts    | 152 ++++++++++++++++++
 .../frontend/src/pages/flash/flash-index.vue  |   3 +-
 packages/misskey-js/etc/misskey-js.api.md     |   4 +
 packages/misskey-js/src/autogen/endpoint.ts   |   3 +-
 packages/misskey-js/src/autogen/entities.ts   |   1 +
 packages/misskey-js/src/autogen/types.ts      |  10 ++
 12 files changed, 262 insertions(+), 25 deletions(-)
 create mode 100644 packages/backend/src/core/FlashService.ts
 create mode 100644 packages/backend/test/unit/FlashService.ts

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 04acc11ac3..6a9143ea1b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -12,6 +12,7 @@
 - Enhance: セキュリティ向上のため、サインイン時もCAPTCHAを求めるようになりました
 - Enhance: 依存関係の更新
 - Enhance: l10nの更新
+- Enhance: Playの「人気」タブで10件以上表示可能に #14399
 - Fix: 連合のホワイトリストが正常に登録されない問題を修正
 
 ### Client
diff --git a/packages/backend/src/core/CoreModule.ts b/packages/backend/src/core/CoreModule.ts
index 3b3c35f976..734d135648 100644
--- a/packages/backend/src/core/CoreModule.ts
+++ b/packages/backend/src/core/CoreModule.ts
@@ -14,6 +14,7 @@ import { AbuseReportNotificationService } from '@/core/AbuseReportNotificationSe
 import { SystemWebhookService } from '@/core/SystemWebhookService.js';
 import { UserSearchService } from '@/core/UserSearchService.js';
 import { WebhookTestService } from '@/core/WebhookTestService.js';
+import { FlashService } from '@/core/FlashService.js';
 import { AccountMoveService } from './AccountMoveService.js';
 import { AccountUpdateService } from './AccountUpdateService.js';
 import { AiService } from './AiService.js';
@@ -217,6 +218,7 @@ const $SystemWebhookService: Provider = { provide: 'SystemWebhookService', useEx
 const $WebhookTestService: Provider = { provide: 'WebhookTestService', useExisting: WebhookTestService };
 const $UtilityService: Provider = { provide: 'UtilityService', useExisting: UtilityService };
 const $FileInfoService: Provider = { provide: 'FileInfoService', useExisting: FileInfoService };
+const $FlashService: Provider = { provide: 'FlashService', useExisting: FlashService };
 const $SearchService: Provider = { provide: 'SearchService', useExisting: SearchService };
 const $ClipService: Provider = { provide: 'ClipService', useExisting: ClipService };
 const $FeaturedService: Provider = { provide: 'FeaturedService', useExisting: FeaturedService };
@@ -367,6 +369,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
 		WebhookTestService,
 		UtilityService,
 		FileInfoService,
+		FlashService,
 		SearchService,
 		ClipService,
 		FeaturedService,
@@ -513,6 +516,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
 		$WebhookTestService,
 		$UtilityService,
 		$FileInfoService,
+		$FlashService,
 		$SearchService,
 		$ClipService,
 		$FeaturedService,
@@ -660,6 +664,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
 		WebhookTestService,
 		UtilityService,
 		FileInfoService,
+		FlashService,
 		SearchService,
 		ClipService,
 		FeaturedService,
diff --git a/packages/backend/src/core/FlashService.ts b/packages/backend/src/core/FlashService.ts
new file mode 100644
index 0000000000..2a98225382
--- /dev/null
+++ b/packages/backend/src/core/FlashService.ts
@@ -0,0 +1,40 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Inject, Injectable } from '@nestjs/common';
+import { DI } from '@/di-symbols.js';
+import { type FlashsRepository } from '@/models/_.js';
+
+/**
+ * MisskeyPlay関係のService
+ */
+@Injectable()
+export class FlashService {
+	constructor(
+		@Inject(DI.flashsRepository)
+		private flashRepository: FlashsRepository,
+	) {
+	}
+
+	/**
+	 * 人気のあるPlay一覧を取得する.
+	 */
+	public async featured(opts?: { offset?: number, limit: number }) {
+		const builder = this.flashRepository.createQueryBuilder('flash')
+			.andWhere('flash.likedCount > 0')
+			.andWhere('flash.visibility = :visibility', { visibility: 'public' })
+			.addOrderBy('flash.likedCount', 'DESC')
+			.addOrderBy('flash.updatedAt', 'DESC')
+			.addOrderBy('flash.id', 'DESC');
+
+		if (opts?.offset) {
+			builder.skip(opts.offset);
+		}
+
+		builder.take(opts?.limit ?? 10);
+
+		return await builder.getMany();
+	}
+}
diff --git a/packages/backend/src/core/entities/FlashEntityService.ts b/packages/backend/src/core/entities/FlashEntityService.ts
index 4aa7104c1e..0cdcf3310a 100644
--- a/packages/backend/src/core/entities/FlashEntityService.ts
+++ b/packages/backend/src/core/entities/FlashEntityService.ts
@@ -5,10 +5,8 @@
 
 import { Inject, Injectable } from '@nestjs/common';
 import { DI } from '@/di-symbols.js';
-import type { FlashsRepository, FlashLikesRepository } from '@/models/_.js';
-import { awaitAll } from '@/misc/prelude/await-all.js';
+import type { FlashLikesRepository, FlashsRepository } from '@/models/_.js';
 import type { Packed } from '@/misc/json-schema.js';
-import type { } from '@/models/Blocking.js';
 import type { MiUser } from '@/models/User.js';
 import type { MiFlash } from '@/models/Flash.js';
 import { bindThis } from '@/decorators.js';
@@ -20,10 +18,8 @@ export class FlashEntityService {
 	constructor(
 		@Inject(DI.flashsRepository)
 		private flashsRepository: FlashsRepository,
-
 		@Inject(DI.flashLikesRepository)
 		private flashLikesRepository: FlashLikesRepository,
-
 		private userEntityService: UserEntityService,
 		private idService: IdService,
 	) {
@@ -34,25 +30,36 @@ export class FlashEntityService {
 		src: MiFlash['id'] | MiFlash,
 		me?: { id: MiUser['id'] } | null | undefined,
 		hint?: {
-			packedUser?: Packed<'UserLite'>
+			packedUser?: Packed<'UserLite'>,
+			likedFlashIds?: MiFlash['id'][],
 		},
 	): Promise<Packed<'Flash'>> {
 		const meId = me ? me.id : null;
 		const flash = typeof src === 'object' ? src : await this.flashsRepository.findOneByOrFail({ id: src });
 
-		return await awaitAll({
+		// { schema: 'UserDetailed' } すると無限ループするので注意
+		const user = hint?.packedUser ?? await this.userEntityService.pack(flash.user ?? flash.userId, me);
+
+		let isLiked = false;
+		if (meId) {
+			isLiked = hint?.likedFlashIds
+				? hint.likedFlashIds.includes(flash.id)
+				: await this.flashLikesRepository.exists({ where: { flashId: flash.id, userId: meId } });
+		}
+
+		return {
 			id: flash.id,
 			createdAt: this.idService.parse(flash.id).date.toISOString(),
 			updatedAt: flash.updatedAt.toISOString(),
 			userId: flash.userId,
-			user: hint?.packedUser ?? this.userEntityService.pack(flash.user ?? flash.userId, me), // { schema: 'UserDetailed' } すると無限ループするので注意
+			user: user,
 			title: flash.title,
 			summary: flash.summary,
 			script: flash.script,
 			visibility: flash.visibility,
 			likedCount: flash.likedCount,
-			isLiked: meId ? await this.flashLikesRepository.exists({ where: { flashId: flash.id, userId: meId } }) : undefined,
-		});
+			isLiked: isLiked,
+		};
 	}
 
 	@bindThis
@@ -63,7 +70,19 @@ export class FlashEntityService {
 		const _users = flashes.map(({ user, userId }) => user ?? userId);
 		const _userMap = await this.userEntityService.packMany(_users, me)
 			.then(users => new Map(users.map(u => [u.id, u])));
-		return Promise.all(flashes.map(flash => this.pack(flash, me, { packedUser: _userMap.get(flash.userId) })));
+		const _likedFlashIds = me
+			? await this.flashLikesRepository.createQueryBuilder('flashLike')
+				.select('flashLike.flashId')
+				.where('flashLike.userId = :userId', { userId: me.id })
+				.getRawMany<{ flashLike_flashId: string }>()
+				.then(likes => [...new Set(likes.map(like => like.flashLike_flashId))])
+			: [];
+		return Promise.all(
+			flashes.map(flash => this.pack(flash, me, {
+				packedUser: _userMap.get(flash.userId),
+				likedFlashIds: _likedFlashIds,
+			})),
+		);
 	}
 }
 
diff --git a/packages/backend/src/models/Flash.ts b/packages/backend/src/models/Flash.ts
index a1469a0d94..5db7dca992 100644
--- a/packages/backend/src/models/Flash.ts
+++ b/packages/backend/src/models/Flash.ts
@@ -7,6 +7,9 @@ import { Entity, Index, JoinColumn, Column, PrimaryColumn, ManyToOne } from 'typ
 import { id } from './util/id.js';
 import { MiUser } from './User.js';
 
+export const flashVisibility = ['public', 'private'] as const;
+export type FlashVisibility = typeof flashVisibility[number];
+
 @Entity('flash')
 export class MiFlash {
 	@PrimaryColumn(id())
@@ -63,5 +66,5 @@ export class MiFlash {
 	@Column('varchar', {
 		length: 512, default: 'public',
 	})
-	public visibility: 'public' | 'private';
+	public visibility: FlashVisibility;
 }
diff --git a/packages/backend/src/server/api/endpoints/flash/featured.ts b/packages/backend/src/server/api/endpoints/flash/featured.ts
index c2d6ab5085..9a0cb461f2 100644
--- a/packages/backend/src/server/api/endpoints/flash/featured.ts
+++ b/packages/backend/src/server/api/endpoints/flash/featured.ts
@@ -8,6 +8,7 @@ import type { FlashsRepository } from '@/models/_.js';
 import { Endpoint } from '@/server/api/endpoint-base.js';
 import { FlashEntityService } from '@/core/entities/FlashEntityService.js';
 import { DI } from '@/di-symbols.js';
+import { FlashService } from '@/core/FlashService.js';
 
 export const meta = {
 	tags: ['flash'],
@@ -27,26 +28,25 @@ export const meta = {
 
 export const paramDef = {
 	type: 'object',
-	properties: {},
+	properties: {
+		offset: { type: 'integer', minimum: 0, default: 0 },
+		limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
+	},
 	required: [],
 } as const;
 
 @Injectable()
 export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
 	constructor(
-		@Inject(DI.flashsRepository)
-		private flashsRepository: FlashsRepository,
-
+		private flashService: FlashService,
 		private flashEntityService: FlashEntityService,
 	) {
 		super(meta, paramDef, async (ps, me) => {
-			const query = this.flashsRepository.createQueryBuilder('flash')
-				.andWhere('flash.likedCount > 0')
-				.orderBy('flash.likedCount', 'DESC');
-
-			const flashs = await query.limit(10).getMany();
-
-			return await this.flashEntityService.packMany(flashs, me);
+			const result = await this.flashService.featured({
+				offset: ps.offset,
+				limit: ps.limit,
+			});
+			return await this.flashEntityService.packMany(result, me);
 		});
 	}
 }
diff --git a/packages/backend/test/unit/FlashService.ts b/packages/backend/test/unit/FlashService.ts
new file mode 100644
index 0000000000..12ffaf3421
--- /dev/null
+++ b/packages/backend/test/unit/FlashService.ts
@@ -0,0 +1,152 @@
+/* eslint-disable @typescript-eslint/no-unused-vars */
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Test, TestingModule } from '@nestjs/testing';
+import { FlashService } from '@/core/FlashService.js';
+import { IdService } from '@/core/IdService.js';
+import { FlashsRepository, MiFlash, MiUser, UserProfilesRepository, UsersRepository } from '@/models/_.js';
+import { DI } from '@/di-symbols.js';
+import { GlobalModule } from '@/GlobalModule.js';
+
+describe('FlashService', () => {
+	let app: TestingModule;
+	let service: FlashService;
+
+	// --------------------------------------------------------------------------------------
+
+	let flashsRepository: FlashsRepository;
+	let usersRepository: UsersRepository;
+	let userProfilesRepository: UserProfilesRepository;
+	let idService: IdService;
+
+	// --------------------------------------------------------------------------------------
+
+	let root: MiUser;
+	let alice: MiUser;
+	let bob: MiUser;
+
+	// --------------------------------------------------------------------------------------
+
+	async function createFlash(data: Partial<MiFlash>) {
+		return flashsRepository.insert({
+			id: idService.gen(),
+			updatedAt: new Date(),
+			userId: root.id,
+			title: 'title',
+			summary: 'summary',
+			script: 'script',
+			permissions: [],
+			likedCount: 0,
+			...data,
+		}).then(x => flashsRepository.findOneByOrFail(x.identifiers[0]));
+	}
+
+	async function createUser(data: Partial<MiUser> = {}) {
+		const user = await usersRepository
+			.insert({
+				id: idService.gen(),
+				...data,
+			})
+			.then(x => usersRepository.findOneByOrFail(x.identifiers[0]));
+
+		await userProfilesRepository.insert({
+			userId: user.id,
+		});
+
+		return user;
+	}
+
+	// --------------------------------------------------------------------------------------
+
+	beforeEach(async () => {
+		app = await Test.createTestingModule({
+			imports: [
+				GlobalModule,
+			],
+			providers: [
+				FlashService,
+				IdService,
+			],
+		}).compile();
+
+		service = app.get(FlashService);
+
+		flashsRepository = app.get(DI.flashsRepository);
+		usersRepository = app.get(DI.usersRepository);
+		userProfilesRepository = app.get(DI.userProfilesRepository);
+		idService = app.get(IdService);
+
+		root = await createUser({ username: 'root', usernameLower: 'root', isRoot: true });
+		alice = await createUser({ username: 'alice', usernameLower: 'alice', isRoot: false });
+		bob = await createUser({ username: 'bob', usernameLower: 'bob', isRoot: false });
+	});
+
+	afterEach(async () => {
+		await usersRepository.delete({});
+		await userProfilesRepository.delete({});
+		await flashsRepository.delete({});
+	});
+
+	afterAll(async () => {
+		await app.close();
+	});
+
+	// --------------------------------------------------------------------------------------
+
+	describe('featured', () => {
+		test('should return featured flashes', async () => {
+			const flash1 = await createFlash({ likedCount: 1 });
+			const flash2 = await createFlash({ likedCount: 2 });
+			const flash3 = await createFlash({ likedCount: 3 });
+
+			const result = await service.featured({
+				offset: 0,
+				limit: 10,
+			});
+
+			expect(result).toEqual([flash3, flash2, flash1]);
+		});
+
+		test('should return featured flashes public visibility only', async () => {
+			const flash1 = await createFlash({ likedCount: 1, visibility: 'public' });
+			const flash2 = await createFlash({ likedCount: 2, visibility: 'public' });
+			const flash3 = await createFlash({ likedCount: 3, visibility: 'private' });
+
+			const result = await service.featured({
+				offset: 0,
+				limit: 10,
+			});
+
+			expect(result).toEqual([flash2, flash1]);
+		});
+
+		test('should return featured flashes with offset', async () => {
+			const flash1 = await createFlash({ likedCount: 1 });
+			const flash2 = await createFlash({ likedCount: 2 });
+			const flash3 = await createFlash({ likedCount: 3 });
+
+			const result = await service.featured({
+				offset: 1,
+				limit: 10,
+			});
+
+			expect(result).toEqual([flash2, flash1]);
+		});
+
+		test('should return featured flashes with limit', async () => {
+			const flash1 = await createFlash({ likedCount: 1 });
+			const flash2 = await createFlash({ likedCount: 2 });
+			const flash3 = await createFlash({ likedCount: 3 });
+
+			const result = await service.featured({
+				offset: 0,
+				limit: 2,
+			});
+
+			expect(result).toEqual([flash3, flash2]);
+		});
+	});
+});
diff --git a/packages/frontend/src/pages/flash/flash-index.vue b/packages/frontend/src/pages/flash/flash-index.vue
index f63a799365..2b85489706 100644
--- a/packages/frontend/src/pages/flash/flash-index.vue
+++ b/packages/frontend/src/pages/flash/flash-index.vue
@@ -55,7 +55,8 @@ const tab = ref('featured');
 
 const featuredFlashsPagination = {
 	endpoint: 'flash/featured' as const,
-	noPaging: true,
+	limit: 5,
+	offsetMode: true,
 };
 const myFlashsPagination = {
 	endpoint: 'flash/my' as const,
diff --git a/packages/misskey-js/etc/misskey-js.api.md b/packages/misskey-js/etc/misskey-js.api.md
index 732352abd8..de52be3a61 100644
--- a/packages/misskey-js/etc/misskey-js.api.md
+++ b/packages/misskey-js/etc/misskey-js.api.md
@@ -1680,6 +1680,7 @@ declare namespace entities {
         FlashCreateRequest,
         FlashCreateResponse,
         FlashDeleteRequest,
+        FlashFeaturedRequest,
         FlashFeaturedResponse,
         FlashLikeRequest,
         FlashShowRequest,
@@ -1929,6 +1930,9 @@ type FlashCreateResponse = operations['flash___create']['responses']['200']['con
 // @public (undocumented)
 type FlashDeleteRequest = operations['flash___delete']['requestBody']['content']['application/json'];
 
+// @public (undocumented)
+type FlashFeaturedRequest = operations['flash___featured']['requestBody']['content']['application/json'];
+
 // @public (undocumented)
 type FlashFeaturedResponse = operations['flash___featured']['responses']['200']['content']['application/json'];
 
diff --git a/packages/misskey-js/src/autogen/endpoint.ts b/packages/misskey-js/src/autogen/endpoint.ts
index 42c74599a5..bf61c20628 100644
--- a/packages/misskey-js/src/autogen/endpoint.ts
+++ b/packages/misskey-js/src/autogen/endpoint.ts
@@ -465,6 +465,7 @@ import type {
 	FlashCreateRequest,
 	FlashCreateResponse,
 	FlashDeleteRequest,
+	FlashFeaturedRequest,
 	FlashFeaturedResponse,
 	FlashLikeRequest,
 	FlashShowRequest,
@@ -889,7 +890,7 @@ export type Endpoints = {
 	'pages/update': { req: PagesUpdateRequest; res: EmptyResponse };
 	'flash/create': { req: FlashCreateRequest; res: FlashCreateResponse };
 	'flash/delete': { req: FlashDeleteRequest; res: EmptyResponse };
-	'flash/featured': { req: EmptyRequest; res: FlashFeaturedResponse };
+	'flash/featured': { req: FlashFeaturedRequest; res: FlashFeaturedResponse };
 	'flash/like': { req: FlashLikeRequest; res: EmptyResponse };
 	'flash/show': { req: FlashShowRequest; res: FlashShowResponse };
 	'flash/unlike': { req: FlashUnlikeRequest; res: EmptyResponse };
diff --git a/packages/misskey-js/src/autogen/entities.ts b/packages/misskey-js/src/autogen/entities.ts
index 87ed653d44..72c7c35ed4 100644
--- a/packages/misskey-js/src/autogen/entities.ts
+++ b/packages/misskey-js/src/autogen/entities.ts
@@ -468,6 +468,7 @@ export type PagesUpdateRequest = operations['pages___update']['requestBody']['co
 export type FlashCreateRequest = operations['flash___create']['requestBody']['content']['application/json'];
 export type FlashCreateResponse = operations['flash___create']['responses']['200']['content']['application/json'];
 export type FlashDeleteRequest = operations['flash___delete']['requestBody']['content']['application/json'];
+export type FlashFeaturedRequest = operations['flash___featured']['requestBody']['content']['application/json'];
 export type FlashFeaturedResponse = operations['flash___featured']['responses']['200']['content']['application/json'];
 export type FlashLikeRequest = operations['flash___like']['requestBody']['content']['application/json'];
 export type FlashShowRequest = operations['flash___show']['requestBody']['content']['application/json'];
diff --git a/packages/misskey-js/src/autogen/types.ts b/packages/misskey-js/src/autogen/types.ts
index 3876a0bfe5..0938973481 100644
--- a/packages/misskey-js/src/autogen/types.ts
+++ b/packages/misskey-js/src/autogen/types.ts
@@ -23799,6 +23799,16 @@ export type operations = {
    * **Credential required**: *No*
    */
   flash___featured: {
+    requestBody: {
+      content: {
+        'application/json': {
+          /** @default 0 */
+          offset?: number;
+          /** @default 10 */
+          limit?: number;
+        };
+      };
+    };
     responses: {
       /** @description OK (with results) */
       200: {

From 043fef9fdf65ee5de9143a14f0626dc4e3f6e54d Mon Sep 17 00:00:00 2001
From: syuilo <4439005+syuilo@users.noreply.github.com>
Date: Sat, 5 Oct 2024 15:19:07 +0900
Subject: [PATCH 046/121] :art:

---
 packages/frontend/src/components/MkMenu.vue | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/packages/frontend/src/components/MkMenu.vue b/packages/frontend/src/components/MkMenu.vue
index 890b99fcc2..14f6bdcc34 100644
--- a/packages/frontend/src/components/MkMenu.vue
+++ b/packages/frontend/src/components/MkMenu.vue
@@ -437,9 +437,11 @@ onBeforeUnmount(() => {
 
 	&.big:not(.asDrawer) {
 		> .menu {
+			min-width: 230px;
+
 			> .item {
 				padding: 6px 20px;
-				font-size: 1em;
+				font-size: 0.95em;
 				line-height: 24px;
 			}
 		}

From d8cb7305ef4d5ad6398d9eb57ece2f3ba7ca73eb Mon Sep 17 00:00:00 2001
From: syuilo <4439005+syuilo@users.noreply.github.com>
Date: Sat, 5 Oct 2024 16:20:15 +0900
Subject: [PATCH 047/121] =?UTF-8?q?feat:=20=E9=80=9A=E5=A0=B1=E3=81=AE?=
 =?UTF-8?q?=E5=BC=B7=E5=8C=96=20(#14704)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

* wip

* Update CHANGELOG.md

* lint

* Update types.ts

* wip

* :v:

* Update MkAbuseReport.vue

* tweak
---
 CHANGELOG.md                                  |   3 +
 locales/index.d.ts                            |  55 ++++++--
 locales/ja-JP.yml                             |  15 ++-
 .../1728085812127-refine-abuse-user-report.js |  18 +++
 .../backend/src/core/AbuseReportService.ts    |  80 ++++++++---
 .../backend/src/core/WebhookTestService.ts    |   2 +
 .../entities/AbuseUserReportEntityService.ts  |   2 +
 .../backend/src/models/AbuseUserReport.ts     |  18 +++
 .../backend/src/server/api/EndpointsModule.ts |   8 ++
 packages/backend/src/server/api/endpoints.ts  |   4 +
 .../admin/forward-abuse-user-report.ts        |  55 ++++++++
 .../admin/resolve-abuse-user-report.ts        |   4 +-
 .../admin/update-abuse-user-report.ts         |  58 ++++++++
 packages/backend/src/types.ts                 |  15 ++-
 .../backend/test/e2e/synalio/abuse-report.ts  |   6 -
 .../frontend/src/components/MkAbuseReport.vue |  74 ++++++++--
 packages/frontend/src/pages/admin-user.vue    |   3 +-
 packages/frontend/src/pages/admin/abuses.vue  |  11 +-
 .../src/pages/admin/modlog.ModLog.vue         |   5 +
 packages/frontend/src/pages/instance-info.vue |   1 +
 packages/frontend/src/pages/user/home.vue     |   3 +-
 packages/frontend/src/store.ts                |   4 +
 packages/misskey-js/etc/misskey-js.api.md     |  16 ++-
 .../misskey-js/src/autogen/apiClientJSDoc.ts  |  22 +++
 packages/misskey-js/src/autogen/endpoint.ts   |   4 +
 packages/misskey-js/src/autogen/entities.ts   |   2 +
 packages/misskey-js/src/autogen/types.ts      | 127 +++++++++++++++++-
 packages/misskey-js/src/consts.ts             |  15 ++-
 packages/misskey-js/src/entities.ts           |   6 +
 29 files changed, 574 insertions(+), 62 deletions(-)
 create mode 100644 packages/backend/migration/1728085812127-refine-abuse-user-report.js
 create mode 100644 packages/backend/src/server/api/endpoints/admin/forward-abuse-user-report.ts
 create mode 100644 packages/backend/src/server/api/endpoints/admin/update-abuse-user-report.ts

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6a9143ea1b..3fd1b7f899 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -9,6 +9,9 @@
 
 ### General
 - Feat: サーバー初期設定時に初期パスワードを設定できるように
+- Feat: 通報にモデレーションノートを残せるように
+- Feat: 通報の解決種別を設定できるように
+- Enhance: 通報の解決と転送を個別に行えるように
 - Enhance: セキュリティ向上のため、サインイン時もCAPTCHAを求めるようになりました
 - Enhance: 依存関係の更新
 - Enhance: l10nの更新
diff --git a/locales/index.d.ts b/locales/index.d.ts
index 1a0547ebc6..d502c5b432 100644
--- a/locales/index.d.ts
+++ b/locales/index.d.ts
@@ -1834,6 +1834,10 @@ export interface Locale extends ILocale {
      * モデレーションノート
      */
     "moderationNote": string;
+    /**
+     * モデレーター間でだけ共有されるメモを記入することができます。
+     */
+    "moderationNoteDescription": string;
     /**
      * モデレーションノートを追加する
      */
@@ -2894,22 +2898,10 @@ export interface Locale extends ILocale {
      * 通報元
      */
     "reporterOrigin": string;
-    /**
-     * リモートサーバーに通報を転送する
-     */
-    "forwardReport": string;
-    /**
-     * リモートサーバーからはあなたの情報は見れず、匿名のシステムアカウントとして表示されます。
-     */
-    "forwardReportIsAnonymous": string;
     /**
      * 送信
      */
     "send": string;
-    /**
-     * 対応済みにする
-     */
-    "abuseMarkAsResolved": string;
     /**
      * 新しいタブで開く
      */
@@ -5170,6 +5162,37 @@ export interface Locale extends ILocale {
      * フォロワーへのメッセージ
      */
     "messageToFollower": string;
+    /**
+     * 対象
+     */
+    "target": string;
+    "_abuseUserReport": {
+        /**
+         * 転送
+         */
+        "forward": string;
+        /**
+         * 匿名のシステムアカウントとして、リモートサーバーに通報を転送します。
+         */
+        "forwardDescription": string;
+        /**
+         * 解決
+         */
+        "resolve": string;
+        /**
+         * 是認
+         */
+        "accept": string;
+        /**
+         * 否認
+         */
+        "reject": string;
+        /**
+         * 内容が正当である通報に対応した場合は「是認」を選択し、肯定的にケースが解決されたことをマークします。
+         * 内容が正当でない通報の場合は「否認」を選択し、否定的にケースが解決されたことをマークします。
+         */
+        "resolveTutorial": string;
+    };
     "_delivery": {
         /**
          * 配信状態
@@ -9785,6 +9808,14 @@ export interface Locale extends ILocale {
          * 通報を解決
          */
         "resolveAbuseReport": string;
+        /**
+         * 通報を転送
+         */
+        "forwardAbuseReport": string;
+        /**
+         * 通報のモデレーションノート更新
+         */
+        "updateAbuseReportNote": string;
         /**
          * 招待コードを作成
          */
diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml
index 92014c8abc..678bc7e66b 100644
--- a/locales/ja-JP.yml
+++ b/locales/ja-JP.yml
@@ -454,6 +454,7 @@ totpDescription: "認証アプリを使ってワンタイムパスワードを
 moderator: "モデレーター"
 moderation: "モデレーション"
 moderationNote: "モデレーションノート"
+moderationNoteDescription: "モデレーター間でだけ共有されるメモを記入することができます。"
 addModerationNote: "モデレーションノートを追加する"
 moderationLogs: "モデログ"
 nUsersMentioned: "{n}人が投稿"
@@ -719,10 +720,7 @@ abuseReported: "内容が送信されました。ご報告ありがとうござ
 reporter: "通報者"
 reporteeOrigin: "通報先"
 reporterOrigin: "通報元"
-forwardReport: "リモートサーバーに通報を転送する"
-forwardReportIsAnonymous: "リモートサーバーからはあなたの情報は見れず、匿名のシステムアカウントとして表示されます。"
 send: "送信"
-abuseMarkAsResolved: "対応済みにする"
 openInNewTab: "新しいタブで開く"
 openInSideView: "サイドビューで開く"
 defaultNavigationBehaviour: "デフォルトのナビゲーション"
@@ -1288,6 +1286,15 @@ unknownWebAuthnKey: "登録されていないパスキーです。"
 passkeyVerificationFailed: "パスキーの検証に失敗しました。"
 passkeyVerificationSucceededButPasswordlessLoginDisabled: "パスキーの検証に成功しましたが、パスワードレスログインが無効になっています。"
 messageToFollower: "フォロワーへのメッセージ"
+target: "対象"
+
+_abuseUserReport:
+  forward: "転送"
+  forwardDescription: "匿名のシステムアカウントとして、リモートサーバーに通報を転送します。"
+  resolve: "解決"
+  accept: "是認"
+  reject: "否認"
+  resolveTutorial: "内容が正当である通報に対応した場合は「是認」を選択し、肯定的にケースが解決されたことをマークします。\n内容が正当でない通報の場合は「否認」を選択し、否定的にケースが解決されたことをマークします。"
 
 _delivery:
   status: "配信状態"
@@ -2593,6 +2600,8 @@ _moderationLogTypes:
   markSensitiveDriveFile: "ファイルをセンシティブ付与"
   unmarkSensitiveDriveFile: "ファイルをセンシティブ解除"
   resolveAbuseReport: "通報を解決"
+  forwardAbuseReport: "通報を転送"
+  updateAbuseReportNote: "通報のモデレーションノート更新"
   createInvitation: "招待コードを作成"
   createAd: "広告を作成"
   deleteAd: "広告を削除"
diff --git a/packages/backend/migration/1728085812127-refine-abuse-user-report.js b/packages/backend/migration/1728085812127-refine-abuse-user-report.js
new file mode 100644
index 0000000000..57cbfdcf6d
--- /dev/null
+++ b/packages/backend/migration/1728085812127-refine-abuse-user-report.js
@@ -0,0 +1,18 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+export class RefineAbuseUserReport1728085812127 {
+    name = 'RefineAbuseUserReport1728085812127'
+
+    async up(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "abuse_user_report" ADD "moderationNote" character varying(8192) NOT NULL DEFAULT ''`);
+        await queryRunner.query(`ALTER TABLE "abuse_user_report" ADD "resolvedAs" character varying(128)`);
+    }
+
+    async down(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "abuse_user_report" DROP COLUMN "resolvedAs"`);
+        await queryRunner.query(`ALTER TABLE "abuse_user_report" DROP COLUMN "moderationNote"`);
+    }
+}
diff --git a/packages/backend/src/core/AbuseReportService.ts b/packages/backend/src/core/AbuseReportService.ts
index 69c51509ba..cddfe5eb81 100644
--- a/packages/backend/src/core/AbuseReportService.ts
+++ b/packages/backend/src/core/AbuseReportService.ts
@@ -20,8 +20,10 @@ export class AbuseReportService {
 	constructor(
 		@Inject(DI.abuseUserReportsRepository)
 		private abuseUserReportsRepository: AbuseUserReportsRepository,
+
 		@Inject(DI.usersRepository)
 		private usersRepository: UsersRepository,
+
 		private idService: IdService,
 		private abuseReportNotificationService: AbuseReportNotificationService,
 		private queueService: QueueService,
@@ -77,16 +79,16 @@ export class AbuseReportService {
 	 * - SystemWebhook
 	 *
 	 * @param params 通報内容. もし複数件の通報に対応した時のために、あらかじめ複数件を処理できる前提で考える
-	 * @param operator 通報を処理したユーザ
+	 * @param moderator 通報を処理したユーザ
 	 * @see AbuseReportNotificationService.notify
 	 */
 	@bindThis
 	public async resolve(
 		params: {
 			reportId: string;
-			forward: boolean;
+			resolvedAs: MiAbuseUserReport['resolvedAs'];
 		}[],
-		operator: MiUser,
+		moderator: MiUser,
 	) {
 		const paramsMap = new Map(params.map(it => [it.reportId, it]));
 		const reports = await this.abuseUserReportsRepository.findBy({
@@ -99,25 +101,15 @@ export class AbuseReportService {
 
 			await this.abuseUserReportsRepository.update(report.id, {
 				resolved: true,
-				assigneeId: operator.id,
-				forwarded: ps.forward && report.targetUserHost !== null,
+				assigneeId: moderator.id,
+				resolvedAs: ps.resolvedAs,
 			});
 
-			if (ps.forward && report.targetUserHost != null) {
-				const actor = await this.instanceActorService.getInstanceActor();
-				const targetUser = await this.usersRepository.findOneByOrFail({ id: report.targetUserId });
-
-				// eslint-disable-next-line
-				const flag = this.apRendererService.renderFlag(actor, targetUser.uri!, report.comment);
-				const contextAssignedFlag = this.apRendererService.addContext(flag);
-				this.queueService.deliver(actor, contextAssignedFlag, targetUser.inbox, false);
-			}
-
 			this.moderationLogService
-				.log(operator, 'resolveAbuseReport', {
+				.log(moderator, 'resolveAbuseReport', {
 					reportId: report.id,
 					report: report,
-					forwarded: ps.forward && report.targetUserHost !== null,
+					resolvedAs: ps.resolvedAs,
 				})
 				.then();
 		}
@@ -125,4 +117,58 @@ export class AbuseReportService {
 		return this.abuseUserReportsRepository.findBy({ id: In(reports.map(it => it.id)) })
 			.then(reports => this.abuseReportNotificationService.notifySystemWebhook(reports, 'abuseReportResolved'));
 	}
+
+	@bindThis
+	public async forward(
+		reportId: MiAbuseUserReport['id'],
+		moderator: MiUser,
+	) {
+		const report = await this.abuseUserReportsRepository.findOneByOrFail({ id: reportId });
+
+		if (report.targetUserHost == null) {
+			throw new Error('The target user host is null.');
+		}
+
+		await this.abuseUserReportsRepository.update(report.id, {
+			forwarded: true,
+		});
+
+		const actor = await this.instanceActorService.getInstanceActor();
+		const targetUser = await this.usersRepository.findOneByOrFail({ id: report.targetUserId });
+
+		const flag = this.apRendererService.renderFlag(actor, targetUser.uri!, report.comment);
+		const contextAssignedFlag = this.apRendererService.addContext(flag);
+		this.queueService.deliver(actor, contextAssignedFlag, targetUser.inbox, false);
+
+		this.moderationLogService
+			.log(moderator, 'forwardAbuseReport', {
+				reportId: report.id,
+				report: report,
+			})
+			.then();
+	}
+
+	@bindThis
+	public async update(
+		reportId: MiAbuseUserReport['id'],
+		params: {
+			moderationNote?: MiAbuseUserReport['moderationNote'];
+		},
+		moderator: MiUser,
+	) {
+		const report = await this.abuseUserReportsRepository.findOneByOrFail({ id: reportId });
+
+		await this.abuseUserReportsRepository.update(report.id, {
+			moderationNote: params.moderationNote,
+		});
+
+		if (params.moderationNote != null && report.moderationNote !== params.moderationNote) {
+			this.moderationLogService.log(moderator, 'updateAbuseReportNote', {
+				reportId: report.id,
+				report: report,
+				before: report.moderationNote,
+				after: params.moderationNote,
+			});
+		}
+	}
 }
diff --git a/packages/backend/src/core/WebhookTestService.ts b/packages/backend/src/core/WebhookTestService.ts
index 149c753d4c..4c45b95a64 100644
--- a/packages/backend/src/core/WebhookTestService.ts
+++ b/packages/backend/src/core/WebhookTestService.ts
@@ -35,6 +35,8 @@ function generateAbuseReport(override?: Partial<MiAbuseUserReport>): AbuseUserRe
 		comment: 'This is a dummy report for testing purposes.',
 		targetUserHost: null,
 		reporterHost: null,
+		resolvedAs: null,
+		moderationNote: 'foo',
 		...override,
 	};
 
diff --git a/packages/backend/src/core/entities/AbuseUserReportEntityService.ts b/packages/backend/src/core/entities/AbuseUserReportEntityService.ts
index a13c244c19..70ead890ab 100644
--- a/packages/backend/src/core/entities/AbuseUserReportEntityService.ts
+++ b/packages/backend/src/core/entities/AbuseUserReportEntityService.ts
@@ -53,6 +53,8 @@ export class AbuseUserReportEntityService {
 				schema: 'UserDetailedNotMe',
 			}) : null,
 			forwarded: report.forwarded,
+			resolvedAs: report.resolvedAs,
+			moderationNote: report.moderationNote,
 		});
 	}
 
diff --git a/packages/backend/src/models/AbuseUserReport.ts b/packages/backend/src/models/AbuseUserReport.ts
index 0615fd7eb5..cb5672e4ac 100644
--- a/packages/backend/src/models/AbuseUserReport.ts
+++ b/packages/backend/src/models/AbuseUserReport.ts
@@ -50,6 +50,9 @@ export class MiAbuseUserReport {
 	})
 	public resolved: boolean;
 
+	/**
+	 * リモートサーバーに転送したかどうか
+	 */
 	@Column('boolean', {
 		default: false,
 	})
@@ -60,6 +63,21 @@ export class MiAbuseUserReport {
 	})
 	public comment: string;
 
+	@Column('varchar', {
+		length: 8192, default: '',
+	})
+	public moderationNote: string;
+
+	/**
+	 * accept 是認 ... 通報内容が正当であり、肯定的に対応された
+	 * reject 否認 ... 通報内容が正当でなく、否定的に対応された
+	 * null ... その他
+	 */
+	@Column('varchar', {
+		length: 128, nullable: true,
+	})
+	public resolvedAs: 'accept' | 'reject' | null;
+
 	//#region Denormalized fields
 	@Index()
 	@Column('varchar', {
diff --git a/packages/backend/src/server/api/EndpointsModule.ts b/packages/backend/src/server/api/EndpointsModule.ts
index 08a0468ab2..3557fa40a5 100644
--- a/packages/backend/src/server/api/EndpointsModule.ts
+++ b/packages/backend/src/server/api/EndpointsModule.ts
@@ -68,6 +68,8 @@ import * as ep___admin_relays_list from './endpoints/admin/relays/list.js';
 import * as ep___admin_relays_remove from './endpoints/admin/relays/remove.js';
 import * as ep___admin_resetPassword from './endpoints/admin/reset-password.js';
 import * as ep___admin_resolveAbuseUserReport from './endpoints/admin/resolve-abuse-user-report.js';
+import * as ep___admin_forwardAbuseUserReport from './endpoints/admin/forward-abuse-user-report.js';
+import * as ep___admin_updateAbuseUserReport from './endpoints/admin/update-abuse-user-report.js';
 import * as ep___admin_sendEmail from './endpoints/admin/send-email.js';
 import * as ep___admin_serverInfo from './endpoints/admin/server-info.js';
 import * as ep___admin_showModerationLogs from './endpoints/admin/show-moderation-logs.js';
@@ -453,6 +455,8 @@ const $admin_relays_list: Provider = { provide: 'ep:admin/relays/list', useClass
 const $admin_relays_remove: Provider = { provide: 'ep:admin/relays/remove', useClass: ep___admin_relays_remove.default };
 const $admin_resetPassword: Provider = { provide: 'ep:admin/reset-password', useClass: ep___admin_resetPassword.default };
 const $admin_resolveAbuseUserReport: Provider = { provide: 'ep:admin/resolve-abuse-user-report', useClass: ep___admin_resolveAbuseUserReport.default };
+const $admin_forwardAbuseUserReport: Provider = { provide: 'ep:admin/forward-abuse-user-report', useClass: ep___admin_forwardAbuseUserReport.default };
+const $admin_updateAbuseUserReport: Provider = { provide: 'ep:admin/update-abuse-user-report', useClass: ep___admin_updateAbuseUserReport.default };
 const $admin_sendEmail: Provider = { provide: 'ep:admin/send-email', useClass: ep___admin_sendEmail.default };
 const $admin_serverInfo: Provider = { provide: 'ep:admin/server-info', useClass: ep___admin_serverInfo.default };
 const $admin_showModerationLogs: Provider = { provide: 'ep:admin/show-moderation-logs', useClass: ep___admin_showModerationLogs.default };
@@ -842,6 +846,8 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__
 		$admin_relays_remove,
 		$admin_resetPassword,
 		$admin_resolveAbuseUserReport,
+		$admin_forwardAbuseUserReport,
+		$admin_updateAbuseUserReport,
 		$admin_sendEmail,
 		$admin_serverInfo,
 		$admin_showModerationLogs,
@@ -1225,6 +1231,8 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__
 		$admin_relays_remove,
 		$admin_resetPassword,
 		$admin_resolveAbuseUserReport,
+		$admin_forwardAbuseUserReport,
+		$admin_updateAbuseUserReport,
 		$admin_sendEmail,
 		$admin_serverInfo,
 		$admin_showModerationLogs,
diff --git a/packages/backend/src/server/api/endpoints.ts b/packages/backend/src/server/api/endpoints.ts
index 2462781f7b..49b07d6ced 100644
--- a/packages/backend/src/server/api/endpoints.ts
+++ b/packages/backend/src/server/api/endpoints.ts
@@ -74,6 +74,8 @@ import * as ep___admin_relays_list from './endpoints/admin/relays/list.js';
 import * as ep___admin_relays_remove from './endpoints/admin/relays/remove.js';
 import * as ep___admin_resetPassword from './endpoints/admin/reset-password.js';
 import * as ep___admin_resolveAbuseUserReport from './endpoints/admin/resolve-abuse-user-report.js';
+import * as ep___admin_forwardAbuseUserReport from './endpoints/admin/forward-abuse-user-report.js';
+import * as ep___admin_updateAbuseUserReport from './endpoints/admin/update-abuse-user-report.js';
 import * as ep___admin_sendEmail from './endpoints/admin/send-email.js';
 import * as ep___admin_serverInfo from './endpoints/admin/server-info.js';
 import * as ep___admin_showModerationLogs from './endpoints/admin/show-moderation-logs.js';
@@ -457,6 +459,8 @@ const eps = [
 	['admin/relays/remove', ep___admin_relays_remove],
 	['admin/reset-password', ep___admin_resetPassword],
 	['admin/resolve-abuse-user-report', ep___admin_resolveAbuseUserReport],
+	['admin/forward-abuse-user-report', ep___admin_forwardAbuseUserReport],
+	['admin/update-abuse-user-report', ep___admin_updateAbuseUserReport],
 	['admin/send-email', ep___admin_sendEmail],
 	['admin/server-info', ep___admin_serverInfo],
 	['admin/show-moderation-logs', ep___admin_showModerationLogs],
diff --git a/packages/backend/src/server/api/endpoints/admin/forward-abuse-user-report.ts b/packages/backend/src/server/api/endpoints/admin/forward-abuse-user-report.ts
new file mode 100644
index 0000000000..3e42c91fed
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/admin/forward-abuse-user-report.ts
@@ -0,0 +1,55 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Inject, Injectable } from '@nestjs/common';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import type { AbuseUserReportsRepository } from '@/models/_.js';
+import { DI } from '@/di-symbols.js';
+import { ApiError } from '@/server/api/error.js';
+import { AbuseReportService } from '@/core/AbuseReportService.js';
+
+export const meta = {
+	tags: ['admin'],
+
+	requireCredential: true,
+	requireModerator: true,
+	kind: 'write:admin:resolve-abuse-user-report',
+
+	errors: {
+		noSuchAbuseReport: {
+			message: 'No such abuse report.',
+			code: 'NO_SUCH_ABUSE_REPORT',
+			id: '8763e21b-d9bc-40be-acf6-54c1a6986493',
+			kind: 'server',
+			httpStatusCode: 404,
+		},
+	},
+} as const;
+
+export const paramDef = {
+	type: 'object',
+	properties: {
+		reportId: { type: 'string', format: 'misskey:id' },
+	},
+	required: ['reportId'],
+} as const;
+
+@Injectable()
+export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
+	constructor(
+		@Inject(DI.abuseUserReportsRepository)
+		private abuseUserReportsRepository: AbuseUserReportsRepository,
+		private abuseReportService: AbuseReportService,
+	) {
+		super(meta, paramDef, async (ps, me) => {
+			const report = await this.abuseUserReportsRepository.findOneBy({ id: ps.reportId });
+			if (!report) {
+				throw new ApiError(meta.errors.noSuchAbuseReport);
+			}
+
+			await this.abuseReportService.forward(report.id, me);
+		});
+	}
+}
diff --git a/packages/backend/src/server/api/endpoints/admin/resolve-abuse-user-report.ts b/packages/backend/src/server/api/endpoints/admin/resolve-abuse-user-report.ts
index 9b79100fcf..554d324ff2 100644
--- a/packages/backend/src/server/api/endpoints/admin/resolve-abuse-user-report.ts
+++ b/packages/backend/src/server/api/endpoints/admin/resolve-abuse-user-report.ts
@@ -32,7 +32,7 @@ export const paramDef = {
 	type: 'object',
 	properties: {
 		reportId: { type: 'string', format: 'misskey:id' },
-		forward: { type: 'boolean', default: false },
+		resolvedAs: { type: 'string', enum: ['accept', 'reject', null], nullable: true },
 	},
 	required: ['reportId'],
 } as const;
@@ -50,7 +50,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 				throw new ApiError(meta.errors.noSuchAbuseReport);
 			}
 
-			await this.abuseReportService.resolve([{ reportId: report.id, forward: ps.forward }], me);
+			await this.abuseReportService.resolve([{ reportId: report.id, resolvedAs: ps.resolvedAs ?? null }], me);
 		});
 	}
 }
diff --git a/packages/backend/src/server/api/endpoints/admin/update-abuse-user-report.ts b/packages/backend/src/server/api/endpoints/admin/update-abuse-user-report.ts
new file mode 100644
index 0000000000..73d4b843f0
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/admin/update-abuse-user-report.ts
@@ -0,0 +1,58 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Inject, Injectable } from '@nestjs/common';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import type { AbuseUserReportsRepository } from '@/models/_.js';
+import { DI } from '@/di-symbols.js';
+import { ApiError } from '@/server/api/error.js';
+import { AbuseReportService } from '@/core/AbuseReportService.js';
+
+export const meta = {
+	tags: ['admin'],
+
+	requireCredential: true,
+	requireModerator: true,
+	kind: 'write:admin:resolve-abuse-user-report',
+
+	errors: {
+		noSuchAbuseReport: {
+			message: 'No such abuse report.',
+			code: 'NO_SUCH_ABUSE_REPORT',
+			id: '15f51cf5-46d1-4b1d-a618-b35bcbed0662',
+			kind: 'server',
+			httpStatusCode: 404,
+		},
+	},
+} as const;
+
+export const paramDef = {
+	type: 'object',
+	properties: {
+		reportId: { type: 'string', format: 'misskey:id' },
+		moderationNote: { type: 'string' },
+	},
+	required: ['reportId'],
+} as const;
+
+@Injectable()
+export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
+	constructor(
+		@Inject(DI.abuseUserReportsRepository)
+		private abuseUserReportsRepository: AbuseUserReportsRepository,
+		private abuseReportService: AbuseReportService,
+	) {
+		super(meta, paramDef, async (ps, me) => {
+			const report = await this.abuseUserReportsRepository.findOneBy({ id: ps.reportId });
+			if (!report) {
+				throw new ApiError(meta.errors.noSuchAbuseReport);
+			}
+
+			await this.abuseReportService.update(report.id, {
+				moderationNote: ps.moderationNote,
+			}, me);
+		});
+	}
+}
diff --git a/packages/backend/src/types.ts b/packages/backend/src/types.ts
index 0389143daf..df3cfee171 100644
--- a/packages/backend/src/types.ts
+++ b/packages/backend/src/types.ts
@@ -99,6 +99,8 @@ export const moderationLogTypes = [
 	'markSensitiveDriveFile',
 	'unmarkSensitiveDriveFile',
 	'resolveAbuseReport',
+	'forwardAbuseReport',
+	'updateAbuseReportNote',
 	'createInvitation',
 	'createAd',
 	'updateAd',
@@ -267,7 +269,18 @@ export type ModerationLogPayloads = {
 	resolveAbuseReport: {
 		reportId: string;
 		report: any;
-		forwarded: boolean;
+		forwarded?: boolean;
+		resolvedAs?: string | null;
+	};
+	forwardAbuseReport: {
+		reportId: string;
+		report: any;
+	};
+	updateAbuseReportNote: {
+		reportId: string;
+		report: any;
+		before: string;
+		after: string;
 	};
 	createInvitation: {
 		invitations: any[];
diff --git a/packages/backend/test/e2e/synalio/abuse-report.ts b/packages/backend/test/e2e/synalio/abuse-report.ts
index 6ce6e47781..c98d199f35 100644
--- a/packages/backend/test/e2e/synalio/abuse-report.ts
+++ b/packages/backend/test/e2e/synalio/abuse-report.ts
@@ -157,7 +157,6 @@ describe('[シナリオ] ユーザ通報', () => {
 			const webhookBody2 = await captureWebhook(async () => {
 				await resolveAbuseReport({
 					reportId: webhookBody1.body.id,
-					forward: false,
 				}, admin);
 			});
 
@@ -214,7 +213,6 @@ describe('[シナリオ] ユーザ通報', () => {
 			const webhookBody2 = await captureWebhook(async () => {
 				await resolveAbuseReport({
 					reportId: abuseReportId,
-					forward: false,
 				}, admin);
 			});
 
@@ -257,7 +255,6 @@ describe('[シナリオ] ユーザ通報', () => {
 			const webhookBody2 = await captureWebhook(async () => {
 				await resolveAbuseReport({
 					reportId: webhookBody1.body.id,
-					forward: false,
 				}, admin);
 			}).catch(e => e.message);
 
@@ -288,7 +285,6 @@ describe('[シナリオ] ユーザ通報', () => {
 			const webhookBody2 = await captureWebhook(async () => {
 				await resolveAbuseReport({
 					reportId: abuseReportId,
-					forward: false,
 				}, admin);
 			}).catch(e => e.message);
 
@@ -319,7 +315,6 @@ describe('[シナリオ] ユーザ通報', () => {
 			const webhookBody2 = await captureWebhook(async () => {
 				await resolveAbuseReport({
 					reportId: abuseReportId,
-					forward: false,
 				}, admin);
 			}).catch(e => e.message);
 
@@ -350,7 +345,6 @@ describe('[シナリオ] ユーザ通報', () => {
 			const webhookBody2 = await captureWebhook(async () => {
 				await resolveAbuseReport({
 					reportId: abuseReportId,
-					forward: false,
 				}, admin);
 			}).catch(e => e.message);
 
diff --git a/packages/frontend/src/components/MkAbuseReport.vue b/packages/frontend/src/components/MkAbuseReport.vue
index c9c629046e..2f0e09fc4b 100644
--- a/packages/frontend/src/components/MkAbuseReport.vue
+++ b/packages/frontend/src/components/MkAbuseReport.vue
@@ -6,26 +6,33 @@ SPDX-License-Identifier: AGPL-3.0-only
 <template>
 <MkFolder>
 	<template #icon>
-		<i v-if="report.resolved" class="ti ti-check" style="color: var(--success)"></i>
+		<i v-if="report.resolved && report.resolvedAs === 'accept'" class="ti ti-check" style="color: var(--success)"></i>
+		<i v-else-if="report.resolved && report.resolvedAs === 'reject'" class="ti ti-x" style="color: var(--error)"></i>
+		<i v-else-if="report.resolved" class="ti ti-slash"></i>
 		<i v-else class="ti ti-exclamation-circle" style="color: var(--warn)"></i>
 	</template>
 	<template #label><MkAcct :user="report.targetUser"/> (by <MkAcct :user="report.reporter"/>)</template>
 	<template #caption>{{ report.comment }}</template>
 	<template #suffix><MkTime :time="report.createdAt"/></template>
-	<template v-if="!report.resolved" #footer>
+	<template #footer>
 		<div class="_buttons">
-			<MkButton primary @click="resolve">{{ i18n.ts.abuseMarkAsResolved }}</MkButton>
-			<template v-if="report.targetUser.host == null || report.resolved">
-				<MkButton primary @click="resolveAndForward">{{ i18n.ts.forwardReport }}</MkButton>
-				<div v-tooltip:dialog="i18n.ts.forwardReportIsAnonymous" class="_button _help"><i class="ti ti-help-circle"></i></div>
+			<template v-if="!report.resolved">
+				<MkButton @click="resolve('accept')"><i class="ti ti-check" style="color: var(--success)"></i> {{ i18n.ts._abuseUserReport.resolve }} ({{ i18n.ts._abuseUserReport.accept }})</MkButton>
+				<MkButton @click="resolve('reject')"><i class="ti ti-x" style="color: var(--error)"></i> {{ i18n.ts._abuseUserReport.resolve }} ({{ i18n.ts._abuseUserReport.reject }})</MkButton>
+				<MkButton @click="resolve(null)"><i class="ti ti-slash"></i> {{ i18n.ts._abuseUserReport.resolve }} ({{ i18n.ts.other }})</MkButton>
 			</template>
+			<template v-if="report.targetUser.host == null">
+				<MkButton :disabled="report.forwarded" primary @click="forward"><i class="ti ti-corner-up-right"></i> {{ i18n.ts._abuseUserReport.forward }}</MkButton>
+				<div v-tooltip:dialog="i18n.ts._abuseUserReport.forwardDescription" class="_button _help"><i class="ti ti-help-circle"></i></div>
+			</template>
+			<button class="_button" style="margin-left: auto; width: 34px;" @click="showMenu"><i class="ti ti-dots"></i></button>
 		</div>
 	</template>
 
 	<div :class="$style.root" class="_gaps_s">
 		<MkFolder :withSpacer="false">
 			<template #icon><MkAvatar :user="report.targetUser" style="width: 18px; height: 18px;"/></template>
-			<template #label>Target: <MkAcct :user="report.targetUser"/></template>
+			<template #label>{{ i18n.ts.target }}: <MkAcct :user="report.targetUser"/></template>
 			<template #suffix>#{{ report.targetUserId.toUpperCase() }}</template>
 
 			<div style="container-type: inline-size;">
@@ -36,7 +43,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 		<MkFolder :defaultOpen="true">
 			<template #icon><i class="ti ti-message-2"></i></template>
 			<template #label>{{ i18n.ts.details }}</template>
-			<div>
+			<div class="_gaps_s">
 				<Mfm :text="report.comment" :linkNavigationBehavior="'window'"/>
 			</div>
 		</MkFolder>
@@ -51,6 +58,17 @@ SPDX-License-Identifier: AGPL-3.0-only
 			</div>
 		</MkFolder>
 
+		<MkFolder :defaultOpen="false">
+			<template #icon><i class="ti ti-message-2"></i></template>
+			<template #label>{{ i18n.ts.moderationNote }}</template>
+			<template #suffix>{{ moderationNote.length > 0 ? '...' : i18n.ts.none }}</template>
+			<div class="_gaps_s">
+				<MkTextarea v-model="moderationNote" manualSave>
+					<template #caption>{{ i18n.ts.moderationNoteDescription }}</template>
+				</MkTextarea>
+			</div>
+		</MkFolder>
+
 		<div v-if="report.assignee">
 			{{ i18n.ts.moderator }}:
 			<MkAcct :user="report.assignee"/>
@@ -60,7 +78,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { provide, ref } from 'vue';
+import { provide, ref, watch } from 'vue';
 import * as Misskey from 'misskey-js';
 import MkButton from '@/components/MkButton.vue';
 import MkSwitch from '@/components/MkSwitch.vue';
@@ -71,6 +89,8 @@ import { dateString } from '@/filters/date.js';
 import MkFolder from '@/components/MkFolder.vue';
 import RouterView from '@/components/global/RouterView.vue';
 import { useRouterFactory } from '@/router/supplier';
+import MkTextarea from '@/components/MkTextarea.vue';
+import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
 
 const props = defineProps<{
 	report: Misskey.entities.AdminAbuseUserReportsResponse[number];
@@ -86,22 +106,48 @@ targetRouter.init();
 const reporterRouter = routerFactory(`/admin/user/${props.report.reporterId}`);
 reporterRouter.init();
 
-function resolve() {
+const moderationNote = ref(props.report.moderationNote ?? '');
+
+watch(moderationNote, async () => {
+	os.apiWithDialog('admin/update-abuse-user-report', {
+		reportId: props.report.id,
+		moderationNote: moderationNote.value,
+	}).then(() => {
+	});
+});
+
+function resolve(resolvedAs) {
 	os.apiWithDialog('admin/resolve-abuse-user-report', {
 		reportId: props.report.id,
+		resolvedAs,
 	}).then(() => {
 		emit('resolved', props.report.id);
 	});
 }
 
-function resolveAndForward() {
-	os.apiWithDialog('admin/resolve-abuse-user-report', {
-		forward: true,
+function forward() {
+	os.apiWithDialog('admin/forward-abuse-user-report', {
 		reportId: props.report.id,
 	}).then(() => {
-		emit('resolved', props.report.id);
+
 	});
 }
+
+function showMenu(ev: MouseEvent) {
+	os.popupMenu([{
+		icon: 'ti ti-id',
+		text: 'Copy ID',
+		action: () => {
+			copyToClipboard(props.report.id);
+		},
+	}, {
+		icon: 'ti ti-json',
+		text: 'Copy JSON',
+		action: () => {
+			copyToClipboard(JSON.stringify(props.report, null, '\t'));
+		},
+	}], ev.currentTarget ?? ev.target);
+}
 </script>
 
 <style lang="scss" module>
diff --git a/packages/frontend/src/pages/admin-user.vue b/packages/frontend/src/pages/admin-user.vue
index d40d1eee58..033634396e 100644
--- a/packages/frontend/src/pages/admin-user.vue
+++ b/packages/frontend/src/pages/admin-user.vue
@@ -53,6 +53,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 				<MkTextarea v-model="moderationNote" manualSave>
 					<template #label>{{ i18n.ts.moderationNote }}</template>
+					<template #caption>{{ i18n.ts.moderationNoteDescription }}</template>
 				</MkTextarea>
 
 				<!--
@@ -205,6 +206,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 <script lang="ts" setup>
 import { computed, defineAsyncComponent, watch, ref } from 'vue';
 import * as Misskey from 'misskey-js';
+import { url } from '@@/js/config.js';
 import MkChart from '@/components/MkChart.vue';
 import MkObjectView from '@/components/MkObjectView.vue';
 import MkTextarea from '@/components/MkTextarea.vue';
@@ -220,7 +222,6 @@ import MkFileListForAdmin from '@/components/MkFileListForAdmin.vue';
 import MkInfo from '@/components/MkInfo.vue';
 import * as os from '@/os.js';
 import { misskeyApi } from '@/scripts/misskey-api.js';
-import { url } from '@@/js/config.js';
 import { acct } from '@/filters/user.js';
 import { definePageMetadata } from '@/scripts/page-metadata.js';
 import { i18n } from '@/i18n.js';
diff --git a/packages/frontend/src/pages/admin/abuses.vue b/packages/frontend/src/pages/admin/abuses.vue
index 33021ae025..22173bb888 100644
--- a/packages/frontend/src/pages/admin/abuses.vue
+++ b/packages/frontend/src/pages/admin/abuses.vue
@@ -12,6 +12,10 @@ SPDX-License-Identifier: AGPL-3.0-only
 				<MkButton link to="/admin/abuse-report-notification-recipient" primary>{{ i18n.ts.notificationSetting }}</MkButton>
 			</div>
 
+			<MkInfo v-if="!defaultStore.reactiveState.abusesTutorial.value" closable @close="closeTutorial()">
+				{{ i18n.ts._abuseUserReport.resolveTutorial }}
+			</MkInfo>
+
 			<div :class="$style.inputs" class="_gaps">
 				<MkSelect v-model="state" style="margin: 0; flex: 1;">
 					<template #label>{{ i18n.ts.state }}</template>
@@ -56,7 +60,6 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <script lang="ts" setup>
 import { computed, shallowRef, ref } from 'vue';
-
 import XHeader from './_header_.vue';
 import MkSelect from '@/components/MkSelect.vue';
 import MkPagination from '@/components/MkPagination.vue';
@@ -64,6 +67,8 @@ import XAbuseReport from '@/components/MkAbuseReport.vue';
 import { i18n } from '@/i18n.js';
 import { definePageMetadata } from '@/scripts/page-metadata.js';
 import MkButton from '@/components/MkButton.vue';
+import MkInfo from '@/components/MkInfo.vue';
+import { defaultStore } from '@/store.js';
 
 const reports = shallowRef<InstanceType<typeof MkPagination>>();
 
@@ -87,6 +92,10 @@ function resolved(reportId) {
 	reports.value?.removeItem(reportId);
 }
 
+function closeTutorial() {
+	defaultStore.set('abusesTutorial', false);
+}
+
 const headerActions = computed(() => []);
 
 const headerTabs = computed(() => []);
diff --git a/packages/frontend/src/pages/admin/modlog.ModLog.vue b/packages/frontend/src/pages/admin/modlog.ModLog.vue
index 64d7f25845..6cf95e936e 100644
--- a/packages/frontend/src/pages/admin/modlog.ModLog.vue
+++ b/packages/frontend/src/pages/admin/modlog.ModLog.vue
@@ -165,6 +165,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 === 'updateAbuseReportNote'">
+			<div :class="$style.diff">
+				<CodeDiff :context="5" :hideHeader="true" :oldString="log.info.before ?? ''" :newString="log.info.after ?? ''" maxHeight="300px"/>
+			</div>
+		</template>
 
 		<details>
 			<summary>raw</summary>
diff --git a/packages/frontend/src/pages/instance-info.vue b/packages/frontend/src/pages/instance-info.vue
index c69530b343..6cec3f9d45 100644
--- a/packages/frontend/src/pages/instance-info.vue
+++ b/packages/frontend/src/pages/instance-info.vue
@@ -51,6 +51,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 						<MkButton @click="refreshMetadata"><i class="ti ti-refresh"></i> Refresh metadata</MkButton>
 						<MkTextarea v-model="moderationNote" manualSave>
 							<template #label>{{ i18n.ts.moderationNote }}</template>
+							<template #caption>{{ i18n.ts.moderationNoteDescription }}</template>
 						</MkTextarea>
 					</div>
 				</FormSection>
diff --git a/packages/frontend/src/pages/user/home.vue b/packages/frontend/src/pages/user/home.vue
index 93af534a9b..79091e584e 100644
--- a/packages/frontend/src/pages/user/home.vue
+++ b/packages/frontend/src/pages/user/home.vue
@@ -64,6 +64,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 					<div v-if="iAmModerator" class="moderationNote">
 						<MkTextarea v-if="editModerationNote || (moderationNote != null && moderationNote !== '')" v-model="moderationNote" manualSave>
 							<template #label>{{ i18n.ts.moderationNote }}</template>
+							<template #caption>{{ i18n.ts.moderationNoteDescription }}</template>
 						</MkTextarea>
 						<div v-else>
 							<MkButton small @click="editModerationNote = true">{{ i18n.ts.addModerationNote }}</MkButton>
@@ -159,6 +160,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 <script lang="ts" setup>
 import { defineAsyncComponent, computed, onMounted, onUnmounted, nextTick, watch, ref } from 'vue';
 import * as Misskey from 'misskey-js';
+import { getScrollPosition } from '@@/js/scroll.js';
 import MkNote from '@/components/MkNote.vue';
 import MkFollowButton from '@/components/MkFollowButton.vue';
 import MkAccountMoved from '@/components/MkAccountMoved.vue';
@@ -168,7 +170,6 @@ import MkTextarea from '@/components/MkTextarea.vue';
 import MkOmit from '@/components/MkOmit.vue';
 import MkInfo from '@/components/MkInfo.vue';
 import MkButton from '@/components/MkButton.vue';
-import { getScrollPosition } from '@@/js/scroll.js';
 import { getUserMenu } from '@/scripts/get-user-menu.js';
 import number from '@/filters/number.js';
 import { userPage } from '@/filters/user.js';
diff --git a/packages/frontend/src/store.ts b/packages/frontend/src/store.ts
index 1ddcca5afe..9254e71c5c 100644
--- a/packages/frontend/src/store.ts
+++ b/packages/frontend/src/store.ts
@@ -78,6 +78,10 @@ export const defaultStore = markRaw(new Storage('base', {
 			global: false,
 		},
 	},
+	abusesTutorial: {
+		where: 'account',
+		default: false,
+	},
 	keepCw: {
 		where: 'account',
 		default: true,
diff --git a/packages/misskey-js/etc/misskey-js.api.md b/packages/misskey-js/etc/misskey-js.api.md
index de52be3a61..1da8e4e613 100644
--- a/packages/misskey-js/etc/misskey-js.api.md
+++ b/packages/misskey-js/etc/misskey-js.api.md
@@ -213,6 +213,9 @@ type AdminFederationRemoveAllFollowingRequest = operations['admin___federation__
 // @public (undocumented)
 type AdminFederationUpdateInstanceRequest = operations['admin___federation___update-instance']['requestBody']['content']['application/json'];
 
+// @public (undocumented)
+type AdminForwardAbuseUserReportRequest = operations['admin___forward-abuse-user-report']['requestBody']['content']['application/json'];
+
 // @public (undocumented)
 type AdminGetIndexStatsResponse = operations['admin___get-index-stats']['responses']['200']['content']['application/json'];
 
@@ -378,6 +381,9 @@ type AdminUnsetUserBannerRequest = operations['admin___unset-user-banner']['requ
 // @public (undocumented)
 type AdminUnsuspendUserRequest = operations['admin___unsuspend-user']['requestBody']['content']['application/json'];
 
+// @public (undocumented)
+type AdminUpdateAbuseUserReportRequest = operations['admin___update-abuse-user-report']['requestBody']['content']['application/json'];
+
 // @public (undocumented)
 type AdminUpdateMetaRequest = operations['admin___update-meta']['requestBody']['content']['application/json'];
 
@@ -1298,6 +1304,8 @@ declare namespace entities {
         AdminResetPasswordRequest,
         AdminResetPasswordResponse,
         AdminResolveAbuseUserReportRequest,
+        AdminForwardAbuseUserReportRequest,
+        AdminUpdateAbuseUserReportRequest,
         AdminSendEmailRequest,
         AdminServerInfoResponse,
         AdminShowModerationLogsRequest,
@@ -2546,6 +2554,12 @@ type ModerationLog = {
 } | {
     type: 'resolveAbuseReport';
     info: ModerationLogPayloads['resolveAbuseReport'];
+} | {
+    type: 'forwardAbuseReport';
+    info: ModerationLogPayloads['forwardAbuseReport'];
+} | {
+    type: 'updateAbuseReportNote';
+    info: ModerationLogPayloads['updateAbuseReportNote'];
 } | {
     type: 'unsetUserAvatar';
     info: ModerationLogPayloads['unsetUserAvatar'];
@@ -2585,7 +2599,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", "updateRemoteInstanceNote", "markSensitiveDriveFile", "unmarkSensitiveDriveFile", "resolveAbuseReport", "createInvitation", "createAd", "updateAd", "deleteAd", "createAvatarDecoration", "updateAvatarDecoration", "deleteAvatarDecoration", "unsetUserAvatar", "unsetUserBanner", "createSystemWebhook", "updateSystemWebhook", "deleteSystemWebhook", "createAbuseReportNotificationRecipient", "updateAbuseReportNotificationRecipient", "deleteAbuseReportNotificationRecipient", "deleteAccount", "deletePage", "deleteFlash", "deleteGalleryPost"];
+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", "forwardAbuseReport", "updateAbuseReportNote", "createInvitation", "createAd", "updateAd", "deleteAd", "createAvatarDecoration", "updateAvatarDecoration", "deleteAvatarDecoration", "unsetUserAvatar", "unsetUserBanner", "createSystemWebhook", "updateSystemWebhook", "deleteSystemWebhook", "createAbuseReportNotificationRecipient", "updateAbuseReportNotificationRecipient", "deleteAbuseReportNotificationRecipient", "deleteAccount", "deletePage", "deleteFlash", "deleteGalleryPost"];
 
 // @public (undocumented)
 type MuteCreateRequest = operations['mute___create']['requestBody']['content']['application/json'];
diff --git a/packages/misskey-js/src/autogen/apiClientJSDoc.ts b/packages/misskey-js/src/autogen/apiClientJSDoc.ts
index 1d96196d1c..e2c7cbba52 100644
--- a/packages/misskey-js/src/autogen/apiClientJSDoc.ts
+++ b/packages/misskey-js/src/autogen/apiClientJSDoc.ts
@@ -691,6 +691,28 @@ declare module '../api.js' {
       credential?: string | null,
     ): Promise<SwitchCaseResponseType<E, P>>;
 
+    /**
+     * No description provided.
+     * 
+     * **Credential required**: *Yes* / **Permission**: *write:admin:resolve-abuse-user-report*
+     */
+    request<E extends 'admin/forward-abuse-user-report', P extends Endpoints[E]['req']>(
+      endpoint: E,
+      params: P,
+      credential?: string | null,
+    ): Promise<SwitchCaseResponseType<E, P>>;
+
+    /**
+     * No description provided.
+     * 
+     * **Credential required**: *Yes* / **Permission**: *write:admin:resolve-abuse-user-report*
+     */
+    request<E extends 'admin/update-abuse-user-report', P extends Endpoints[E]['req']>(
+      endpoint: E,
+      params: P,
+      credential?: string | null,
+    ): Promise<SwitchCaseResponseType<E, P>>;
+
     /**
      * No description provided.
      * 
diff --git a/packages/misskey-js/src/autogen/endpoint.ts b/packages/misskey-js/src/autogen/endpoint.ts
index bf61c20628..d0367d8496 100644
--- a/packages/misskey-js/src/autogen/endpoint.ts
+++ b/packages/misskey-js/src/autogen/endpoint.ts
@@ -83,6 +83,8 @@ import type {
 	AdminResetPasswordRequest,
 	AdminResetPasswordResponse,
 	AdminResolveAbuseUserReportRequest,
+	AdminForwardAbuseUserReportRequest,
+	AdminUpdateAbuseUserReportRequest,
 	AdminSendEmailRequest,
 	AdminServerInfoResponse,
 	AdminShowModerationLogsRequest,
@@ -639,6 +641,8 @@ export type Endpoints = {
 	'admin/relays/remove': { req: AdminRelaysRemoveRequest; res: EmptyResponse };
 	'admin/reset-password': { req: AdminResetPasswordRequest; res: AdminResetPasswordResponse };
 	'admin/resolve-abuse-user-report': { req: AdminResolveAbuseUserReportRequest; res: EmptyResponse };
+	'admin/forward-abuse-user-report': { req: AdminForwardAbuseUserReportRequest; res: EmptyResponse };
+	'admin/update-abuse-user-report': { req: AdminUpdateAbuseUserReportRequest; res: EmptyResponse };
 	'admin/send-email': { req: AdminSendEmailRequest; res: EmptyResponse };
 	'admin/server-info': { req: EmptyRequest; res: AdminServerInfoResponse };
 	'admin/show-moderation-logs': { req: AdminShowModerationLogsRequest; res: AdminShowModerationLogsResponse };
diff --git a/packages/misskey-js/src/autogen/entities.ts b/packages/misskey-js/src/autogen/entities.ts
index 72c7c35ed4..ced87c4c7e 100644
--- a/packages/misskey-js/src/autogen/entities.ts
+++ b/packages/misskey-js/src/autogen/entities.ts
@@ -86,6 +86,8 @@ export type AdminRelaysRemoveRequest = operations['admin___relays___remove']['re
 export type AdminResetPasswordRequest = operations['admin___reset-password']['requestBody']['content']['application/json'];
 export type AdminResetPasswordResponse = operations['admin___reset-password']['responses']['200']['content']['application/json'];
 export type AdminResolveAbuseUserReportRequest = operations['admin___resolve-abuse-user-report']['requestBody']['content']['application/json'];
+export type AdminForwardAbuseUserReportRequest = operations['admin___forward-abuse-user-report']['requestBody']['content']['application/json'];
+export type AdminUpdateAbuseUserReportRequest = operations['admin___update-abuse-user-report']['requestBody']['content']['application/json'];
 export type AdminSendEmailRequest = operations['admin___send-email']['requestBody']['content']['application/json'];
 export type AdminServerInfoResponse = operations['admin___server-info']['responses']['200']['content']['application/json'];
 export type AdminShowModerationLogsRequest = operations['admin___show-moderation-logs']['requestBody']['content']['application/json'];
diff --git a/packages/misskey-js/src/autogen/types.ts b/packages/misskey-js/src/autogen/types.ts
index 0938973481..43f18bb680 100644
--- a/packages/misskey-js/src/autogen/types.ts
+++ b/packages/misskey-js/src/autogen/types.ts
@@ -576,6 +576,24 @@ export type paths = {
      */
     post: operations['admin___resolve-abuse-user-report'];
   };
+  '/admin/forward-abuse-user-report': {
+    /**
+     * admin/forward-abuse-user-report
+     * @description No description provided.
+     *
+     * **Credential required**: *Yes* / **Permission**: *write:admin:resolve-abuse-user-report*
+     */
+    post: operations['admin___forward-abuse-user-report'];
+  };
+  '/admin/update-abuse-user-report': {
+    /**
+     * admin/update-abuse-user-report
+     * @description No description provided.
+     *
+     * **Credential required**: *Yes* / **Permission**: *write:admin:resolve-abuse-user-report*
+     */
+    post: operations['admin___update-abuse-user-report'];
+  };
   '/admin/send-email': {
     /**
      * admin/send-email
@@ -8693,8 +8711,113 @@ export type operations = {
         'application/json': {
           /** Format: misskey:id */
           reportId: string;
-          /** @default false */
-          forward?: boolean;
+          /** @enum {string|null} */
+          resolvedAs?: 'accept' | 'reject' | null;
+        };
+      };
+    };
+    responses: {
+      /** @description OK (without any results) */
+      204: {
+        content: never;
+      };
+      /** @description Client error */
+      400: {
+        content: {
+          'application/json': components['schemas']['Error'];
+        };
+      };
+      /** @description Authentication error */
+      401: {
+        content: {
+          'application/json': components['schemas']['Error'];
+        };
+      };
+      /** @description Forbidden error */
+      403: {
+        content: {
+          'application/json': components['schemas']['Error'];
+        };
+      };
+      /** @description I'm Ai */
+      418: {
+        content: {
+          'application/json': components['schemas']['Error'];
+        };
+      };
+      /** @description Internal server error */
+      500: {
+        content: {
+          'application/json': components['schemas']['Error'];
+        };
+      };
+    };
+  };
+  /**
+   * admin/forward-abuse-user-report
+   * @description No description provided.
+   *
+   * **Credential required**: *Yes* / **Permission**: *write:admin:resolve-abuse-user-report*
+   */
+  'admin___forward-abuse-user-report': {
+    requestBody: {
+      content: {
+        'application/json': {
+          /** Format: misskey:id */
+          reportId: string;
+        };
+      };
+    };
+    responses: {
+      /** @description OK (without any results) */
+      204: {
+        content: never;
+      };
+      /** @description Client error */
+      400: {
+        content: {
+          'application/json': components['schemas']['Error'];
+        };
+      };
+      /** @description Authentication error */
+      401: {
+        content: {
+          'application/json': components['schemas']['Error'];
+        };
+      };
+      /** @description Forbidden error */
+      403: {
+        content: {
+          'application/json': components['schemas']['Error'];
+        };
+      };
+      /** @description I'm Ai */
+      418: {
+        content: {
+          'application/json': components['schemas']['Error'];
+        };
+      };
+      /** @description Internal server error */
+      500: {
+        content: {
+          'application/json': components['schemas']['Error'];
+        };
+      };
+    };
+  };
+  /**
+   * admin/update-abuse-user-report
+   * @description No description provided.
+   *
+   * **Credential required**: *Yes* / **Permission**: *write:admin:resolve-abuse-user-report*
+   */
+  'admin___update-abuse-user-report': {
+    requestBody: {
+      content: {
+        'application/json': {
+          /** Format: misskey:id */
+          reportId: string;
+          moderationNote?: string;
         };
       };
     };
diff --git a/packages/misskey-js/src/consts.ts b/packages/misskey-js/src/consts.ts
index b4fbcffa97..c5911a70eb 100644
--- a/packages/misskey-js/src/consts.ts
+++ b/packages/misskey-js/src/consts.ts
@@ -142,6 +142,8 @@ export const moderationLogTypes = [
 	'markSensitiveDriveFile',
 	'unmarkSensitiveDriveFile',
 	'resolveAbuseReport',
+	'forwardAbuseReport',
+	'updateAbuseReportNote',
 	'createInvitation',
 	'createAd',
 	'updateAd',
@@ -330,7 +332,18 @@ export type ModerationLogPayloads = {
 	resolveAbuseReport: {
 		reportId: string;
 		report: ReceivedAbuseReport;
-		forwarded: boolean;
+		forwarded?: boolean;
+		resolvedAs?: string | null;
+	};
+	forwardAbuseReport: {
+		reportId: string;
+		report: ReceivedAbuseReport;
+	};
+	updateAbuseReportNote: {
+		reportId: string;
+		report: ReceivedAbuseReport;
+		before: string;
+		after: string;
 	};
 	createInvitation: {
 		invitations: InviteCode[];
diff --git a/packages/misskey-js/src/entities.ts b/packages/misskey-js/src/entities.ts
index 8bbc9c113b..2ffee40fba 100644
--- a/packages/misskey-js/src/entities.ts
+++ b/packages/misskey-js/src/entities.ts
@@ -153,6 +153,12 @@ export type ModerationLog = {
 } | {
 	type: 'resolveAbuseReport';
 	info: ModerationLogPayloads['resolveAbuseReport'];
+} | {
+	type: 'forwardAbuseReport';
+	info: ModerationLogPayloads['forwardAbuseReport'];
+} | {
+	type: 'updateAbuseReportNote';
+	info: ModerationLogPayloads['updateAbuseReportNote'];
 } | {
 	type: 'unsetUserAvatar';
 	info: ModerationLogPayloads['unsetUserAvatar'];

From 9d026975bcd35339592c62ae86ace1179a55b4af Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=8B=E3=81=A3=E3=81=93=E3=81=8B=E3=82=8A?=
 <67428053+kakkokari-gtyih@users.noreply.github.com>
Date: Sat, 5 Oct 2024 16:20:44 +0900
Subject: [PATCH 048/121] =?UTF-8?q?fix(backend/test):=20#14558=20=E4=BB=A5?=
 =?UTF-8?q?=E9=99=8De2e=E3=83=86=E3=82=B9=E3=83=88=E3=81=8C=E3=81=9F?=
 =?UTF-8?q?=E3=81=BE=E3=81=AB=E5=A4=B1=E6=95=97=E3=81=99=E3=82=8B=E5=95=8F?=
 =?UTF-8?q?=E9=A1=8C=E3=82=92=E4=BF=AE=E6=AD=A3=20(#14709)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

* fix(backend/test): MisskeyIO#727 以降e2eテストがたまに失敗する問題を修正 (MisskeyIO#735)

* :v:

---------

Co-authored-by: まっちゃとーにゅ <17376330+u1-liquid@users.noreply.github.com>
---
 packages/backend/src/core/NoteCreateService.ts                 | 2 +-
 packages/backend/src/queue/processors/InboxProcessorService.ts | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/packages/backend/src/core/NoteCreateService.ts b/packages/backend/src/core/NoteCreateService.ts
index 89e3eafa0e..0ce57f16e6 100644
--- a/packages/backend/src/core/NoteCreateService.ts
+++ b/packages/backend/src/core/NoteCreateService.ts
@@ -218,7 +218,7 @@ export class NoteCreateService implements OnApplicationShutdown {
 		private utilityService: UtilityService,
 		private userBlockingService: UserBlockingService,
 	) {
-		this.updateNotesCountQueue = new CollapsedQueue(60 * 1000 * 5, this.collapseNotesCount, this.performUpdateNotesCount);
+		this.updateNotesCountQueue = new CollapsedQueue(process.env.NODE_ENV !== 'test' ? 60 * 1000 * 5 : 0, this.collapseNotesCount, this.performUpdateNotesCount);
 	}
 
 	@bindThis
diff --git a/packages/backend/src/queue/processors/InboxProcessorService.ts b/packages/backend/src/queue/processors/InboxProcessorService.ts
index 09d51bec72..a77c968395 100644
--- a/packages/backend/src/queue/processors/InboxProcessorService.ts
+++ b/packages/backend/src/queue/processors/InboxProcessorService.ts
@@ -59,7 +59,7 @@ export class InboxProcessorService implements OnApplicationShutdown {
 		private queueLoggerService: QueueLoggerService,
 	) {
 		this.logger = this.queueLoggerService.logger.createSubLogger('inbox');
-		this.updateInstanceQueue = new CollapsedQueue(60 * 1000 * 5, this.collapseUpdateInstanceJobs, this.performUpdateInstance);
+		this.updateInstanceQueue = new CollapsedQueue(process.env.NODE_ENV !== 'test' ? 60 * 1000 * 5 : 0, this.collapseUpdateInstanceJobs, this.performUpdateInstance);
 	}
 
 	@bindThis

From 254c063455ad2886fe780d221cac90fc3da03ecf Mon Sep 17 00:00:00 2001
From: "github-actions[bot]" <github-actions[bot]@users.noreply.github.com>
Date: Sat, 5 Oct 2024 07:31:13 +0000
Subject: [PATCH 049/121] Bump version to 2024.10.0-beta.5

---
 package.json                     | 2 +-
 packages/misskey-js/package.json | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/package.json b/package.json
index 7c01180531..50c42645d3 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
 	"name": "misskey",
-	"version": "2024.10.0-beta.4",
+	"version": "2024.10.0-beta.5",
 	"codename": "nasubi",
 	"repository": {
 		"type": "git",
diff --git a/packages/misskey-js/package.json b/packages/misskey-js/package.json
index 4643516b7b..135ac60873 100644
--- a/packages/misskey-js/package.json
+++ b/packages/misskey-js/package.json
@@ -1,7 +1,7 @@
 {
 	"type": "module",
 	"name": "misskey-js",
-	"version": "2024.10.0-beta.4",
+	"version": "2024.10.0-beta.5",
 	"description": "Misskey SDK for JavaScript",
 	"license": "MIT",
 	"main": "./built/index.js",

From 057a6d731d30de1f2259d140bcd9334166509966 Mon Sep 17 00:00:00 2001
From: syuilo <4439005+syuilo@users.noreply.github.com>
Date: Sat, 5 Oct 2024 18:24:04 +0900
Subject: [PATCH 050/121] :art:

---
 packages/frontend/src/pages/user/home.vue | 5 +++--
 1 file changed, 3 insertions(+), 2 deletions(-)

diff --git a/packages/frontend/src/pages/user/home.vue b/packages/frontend/src/pages/user/home.vue
index 79091e584e..a097b1f0bb 100644
--- a/packages/frontend/src/pages/user/home.vue
+++ b/packages/frontend/src/pages/user/home.vue
@@ -50,7 +50,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 					<div v-if="user.followedMessage != null" class="followedMessage">
 						<MkFukidashi class="fukidashi" :tail="narrow ? 'none' : 'left'" negativeMargin shadow>
 							<div class="messageHeader">{{ i18n.ts.messageToFollower }}</div>
-							<div><Mfm :text="user.followedMessage" :author="user"/></div>
+							<div><MkSparkle><Mfm :text="user.followedMessage" :author="user"/></MkSparkle></div>
 						</MkFukidashi>
 					</div>
 					<div v-if="user.roles.length > 0" class="roles">
@@ -183,6 +183,7 @@ import { misskeyApi } from '@/scripts/misskey-api.js';
 import { isFollowingVisibleForMe, isFollowersVisibleForMe } from '@/scripts/isFfVisibleForMe.js';
 import { useRouter } from '@/router/supplier.js';
 import { getStaticImageUrl } from '@/scripts/media-proxy.js';
+import MkSparkle from '@/components/MkSparkle.vue';
 
 function calcAge(birthdate: string): number {
 	const date = new Date(birthdate);
@@ -473,7 +474,7 @@ onUnmounted(() => {
 
 					> .fukidashi {
 						display: block;
-						--fukidashi-bg: color-mix(in srgb, var(--love), var(--panel) 85%);
+						--fukidashi-bg: color-mix(in srgb, var(--accent), var(--panel) 85%);
 						--fukidashi-radius: 16px;
 						font-size: 0.9em;
 

From ddc799fe3de8024141702e26f4272227a7d94da4 Mon Sep 17 00:00:00 2001
From: syuilo <4439005+syuilo@users.noreply.github.com>
Date: Sat, 5 Oct 2024 18:29:02 +0900
Subject: [PATCH 051/121] fix of d8cb7305ef4d5ad6398d9eb57ece2f3ba7ca73eb

---
 packages/backend/src/core/AbuseReportService.ts | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/packages/backend/src/core/AbuseReportService.ts b/packages/backend/src/core/AbuseReportService.ts
index cddfe5eb81..73baad5499 100644
--- a/packages/backend/src/core/AbuseReportService.ts
+++ b/packages/backend/src/core/AbuseReportService.ts
@@ -129,6 +129,10 @@ export class AbuseReportService {
 			throw new Error('The target user host is null.');
 		}
 
+		if (report.forwarded) {
+			throw new Error('The report has already been forwarded.');
+		}
+
 		await this.abuseUserReportsRepository.update(report.id, {
 			forwarded: true,
 		});

From ddf8e2a3dc61e0dc7b3ffe12e767d41a5b7c4526 Mon Sep 17 00:00:00 2001
From: zyoshoka <107108195+zyoshoka@users.noreply.github.com>
Date: Sat, 5 Oct 2024 18:35:37 +0900
Subject: [PATCH 052/121] fix(backend): correct `admin/abuse-user-reports`
 schema (#14711)

* fix(backend): correct `abuse-user-reports` schema

* Update CHANGELOG.md
---
 CHANGELOG.md                                     |  1 +
 .../api/endpoints/admin/abuse-user-reports.ts    | 16 ++++++++++++++--
 packages/misskey-js/src/autogen/types.ts         |  8 +++++---
 3 files changed, 20 insertions(+), 5 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3fd1b7f899..85f5da28dd 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -26,6 +26,7 @@
 - Enhance: セキュリティ向上のため、ログイン時にメール通知を行うように
 - Enhance: 自分とモデレーター以外のユーザーから二要素認証関連のデータが取得できないように
 - Enhance: 通報および通報解決時に送出されるSystemWebhookにユーザ情報を含めるように ( #14697 )
+- Fix: `admin/abuse-user-reports`エンドポイントのスキーマが間違っていた問題を修正
 
 ## 2024.9.0
 
diff --git a/packages/backend/src/server/api/endpoints/admin/abuse-user-reports.ts b/packages/backend/src/server/api/endpoints/admin/abuse-user-reports.ts
index cf3f257ca6..0dbfaae054 100644
--- a/packages/backend/src/server/api/endpoints/admin/abuse-user-reports.ts
+++ b/packages/backend/src/server/api/endpoints/admin/abuse-user-reports.ts
@@ -71,9 +71,22 @@ export const meta = {
 				},
 				assignee: {
 					type: 'object',
-					nullable: true, optional: true,
+					nullable: true, optional: false,
 					ref: 'UserDetailedNotMe',
 				},
+				forwarded: {
+					type: 'boolean',
+					nullable: false, optional: false,
+				},
+				resolvedAs: {
+					type: 'string',
+					nullable: true, optional: false,
+					enum: ['accept', 'reject', null],
+				},
+				moderationNote: {
+					type: 'string',
+					nullable: false, optional: false,
+				},
 			},
 		},
 	},
@@ -88,7 +101,6 @@ export const paramDef = {
 		state: { type: 'string', nullable: true, default: null },
 		reporterOrigin: { type: 'string', enum: ['combined', 'local', 'remote'], default: 'combined' },
 		targetUserOrigin: { type: 'string', enum: ['combined', 'local', 'remote'], default: 'combined' },
-		forwarded: { type: 'boolean', default: false },
 	},
 	required: [],
 } as const;
diff --git a/packages/misskey-js/src/autogen/types.ts b/packages/misskey-js/src/autogen/types.ts
index 43f18bb680..76ef7ea1fb 100644
--- a/packages/misskey-js/src/autogen/types.ts
+++ b/packages/misskey-js/src/autogen/types.ts
@@ -5270,8 +5270,6 @@ export type operations = {
            * @enum {string}
            */
           targetUserOrigin?: 'combined' | 'local' | 'remote';
-          /** @default false */
-          forwarded?: boolean;
         };
       };
     };
@@ -5298,7 +5296,11 @@ export type operations = {
               assigneeId: string | null;
               reporter: components['schemas']['UserDetailedNotMe'];
               targetUser: components['schemas']['UserDetailedNotMe'];
-              assignee?: components['schemas']['UserDetailedNotMe'] | null;
+              assignee: components['schemas']['UserDetailedNotMe'] | null;
+              forwarded: boolean;
+              /** @enum {string|null} */
+              resolvedAs: 'accept' | 'reject' | null;
+              moderationNote: string;
             })[];
         };
       };

From 7933b6662e3dd205c1799d633762cdef7524456b Mon Sep 17 00:00:00 2001
From: syuilo <4439005+syuilo@users.noreply.github.com>
Date: Sat, 5 Oct 2024 18:57:23 +0900
Subject: [PATCH 053/121] :art:

---
 packages/frontend/src/pages/user/home.vue | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/packages/frontend/src/pages/user/home.vue b/packages/frontend/src/pages/user/home.vue
index a097b1f0bb..111df41127 100644
--- a/packages/frontend/src/pages/user/home.vue
+++ b/packages/frontend/src/pages/user/home.vue
@@ -50,7 +50,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 					<div v-if="user.followedMessage != null" class="followedMessage">
 						<MkFukidashi class="fukidashi" :tail="narrow ? 'none' : 'left'" negativeMargin shadow>
 							<div class="messageHeader">{{ i18n.ts.messageToFollower }}</div>
-							<div><MkSparkle><Mfm :text="user.followedMessage" :author="user"/></MkSparkle></div>
+							<div><MkSparkle><Mfm :plain="true" :text="user.followedMessage" :author="user"/></MkSparkle></div>
 						</MkFukidashi>
 					</div>
 					<div v-if="user.roles.length > 0" class="roles">

From a594d9f26bc928e7b7e474a589f2dba4f05a711f Mon Sep 17 00:00:00 2001
From: syuilo <4439005+syuilo@users.noreply.github.com>
Date: Sat, 5 Oct 2024 19:47:45 +0900
Subject: [PATCH 054/121] make animatedMfm enable by default

---
 packages/frontend/src/store.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/packages/frontend/src/store.ts b/packages/frontend/src/store.ts
index 9254e71c5c..55d36f794f 100644
--- a/packages/frontend/src/store.ts
+++ b/packages/frontend/src/store.ts
@@ -226,7 +226,7 @@ export const defaultStore = markRaw(new Storage('base', {
 	},
 	animatedMfm: {
 		where: 'device',
-		default: false,
+		default: true,
 	},
 	advancedMfm: {
 		where: 'device',

From d2f1d45ea36f16432a4b9df771bf974bbe7ef416 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=8B=E3=81=A3=E3=81=93=E3=81=8B=E3=82=8A?=
 <67428053+kakkokari-gtyih@users.noreply.github.com>
Date: Mon, 7 Oct 2024 09:07:02 +0900
Subject: [PATCH 055/121] =?UTF-8?q?fix(frontend):=20=E3=82=AF=E3=83=A9?=
 =?UTF-8?q?=E3=82=A4=E3=82=A2=E3=83=B3=E3=83=88=E4=B8=8A=E3=81=A7=E3=81=AE?=
 =?UTF-8?q?=E6=99=82=E9=96=93=E3=83=99=E3=83=BC=E3=82=B9=E3=81=AE=E5=AE=9F?=
 =?UTF-8?q?=E7=B8=BE=E7=8D=B2=E5=BE=97=E5=8B=95=E4=BD=9C=E3=81=8C=E5=AE=9F?=
 =?UTF-8?q?=E7=B8=BE=E7=8D=B2=E5=BE=97=E5=BE=8C=E3=82=82=E7=99=BA=E5=8B=95?=
 =?UTF-8?q?=E3=81=97=E3=81=A6=E3=81=84=E3=82=8B=E5=95=8F=E9=A1=8C=E3=82=92?=
 =?UTF-8?q?=E4=BF=AE=E6=AD=A3=20(#14717)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

* Check if time-based achievements are unlocked before initializing them in main-boot

(cherry picked from commit c0702fd92f70782005517c0065048ececa1ef287)

* Update Changelog

---------

Co-authored-by: Evan Paterakis <evan@geopjr.dev>
---
 CHANGELOG.md                            |  2 ++
 packages/frontend/src/boot/main-boot.ts | 28 +++++++++++++++----------
 2 files changed, 19 insertions(+), 11 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 85f5da28dd..405ee7c10a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -21,6 +21,8 @@
 ### Client
 - Enhance: デザインの調整
 - Enhance: ログイン画面の認証フローを改善
+- Fix: クライアント上での時間ベースの実績獲得動作が実績獲得後も発動していた問題を修正  
+  (Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/657)
 
 ### Server
 - Enhance: セキュリティ向上のため、ログイン時にメール通知を行うように
diff --git a/packages/frontend/src/boot/main-boot.ts b/packages/frontend/src/boot/main-boot.ts
index ddd47ca448..76459ab330 100644
--- a/packages/frontend/src/boot/main-boot.ts
+++ b/packages/frontend/src/boot/main-boot.ts
@@ -230,19 +230,25 @@ export async function mainBoot() {
 			claimAchievement('collectAchievements30');
 		}
 
-		window.setInterval(() => {
-			if (Math.floor(Math.random() * 20000) === 0) {
-				claimAchievement('justPlainLucky');
-			}
-		}, 1000 * 10);
+		if (!claimedAchievements.includes('justPlainLucky')) {
+			window.setInterval(() => {
+				if (Math.floor(Math.random() * 20000) === 0) {
+					claimAchievement('justPlainLucky');
+				}
+			}, 1000 * 10);
+		}
 
-		window.setTimeout(() => {
-			claimAchievement('client30min');
-		}, 1000 * 60 * 30);
+		if (!claimedAchievements.includes('client30min')) {
+			window.setTimeout(() => {
+				claimAchievement('client30min');
+			}, 1000 * 60 * 30);
+		}
 
-		window.setTimeout(() => {
-			claimAchievement('client60min');
-		}, 1000 * 60 * 60);
+		if (!claimedAchievements.includes('client60min')) {
+			window.setTimeout(() => {
+				claimAchievement('client60min');
+			}, 1000 * 60 * 60);
+		}
 
 		// 邪魔
 		//const lastUsed = miLocalStorage.getItem('lastUsed');

From 8b2780c730f3a1af2d6856be51a0f37fe80e4f2d Mon Sep 17 00:00:00 2001
From: syuilo <4439005+syuilo@users.noreply.github.com>
Date: Mon, 7 Oct 2024 09:42:35 +0900
Subject: [PATCH 056/121] Update packages/frontend/src/store.ts
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Co-authored-by: かっこかり <67428053+kakkokari-gtyih@users.noreply.github.com>
---
 packages/frontend/src/store.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/packages/frontend/src/store.ts b/packages/frontend/src/store.ts
index 55d36f794f..7bb19aa2d7 100644
--- a/packages/frontend/src/store.ts
+++ b/packages/frontend/src/store.ts
@@ -226,7 +226,7 @@ export const defaultStore = markRaw(new Storage('base', {
 	},
 	animatedMfm: {
 		where: 'device',
-		default: true,
+		default: window.matchMedia('(prefers-reduced-motion)').matches,
 	},
 	advancedMfm: {
 		where: 'device',

From 03fb6880732df7474b8f1bb039e6b3782cf522c5 Mon Sep 17 00:00:00 2001
From: syuilo <4439005+syuilo@users.noreply.github.com>
Date: Mon, 7 Oct 2024 09:44:35 +0900
Subject: [PATCH 057/121] New Crowdin updates (#14695)

* New translations ja-jp.yml (Italian)

* New translations ja-jp.yml (Korean)

* New translations ja-jp.yml (Catalan)

* New translations ja-jp.yml (Chinese Traditional)

* New translations ja-jp.yml (English)

* New translations ja-jp.yml (Portuguese)

* New translations ja-jp.yml (Catalan)

* New translations ja-jp.yml (Italian)

* New translations ja-jp.yml (Russian)

* New translations ja-jp.yml (Chinese Traditional)

* New translations ja-jp.yml (Indonesian)

* New translations ja-jp.yml (Romanian)

* New translations ja-jp.yml (French)

* New translations ja-jp.yml (Spanish)

* New translations ja-jp.yml (Arabic)

* New translations ja-jp.yml (Czech)

* New translations ja-jp.yml (German)

* New translations ja-jp.yml (Korean)

* New translations ja-jp.yml (Polish)

* New translations ja-jp.yml (Slovak)

* New translations ja-jp.yml (Ukrainian)

* New translations ja-jp.yml (Chinese Simplified)

* New translations ja-jp.yml (Vietnamese)

* New translations ja-jp.yml (Bengali)

* New translations ja-jp.yml (Thai)

* New translations ja-jp.yml (Uzbek)

* New translations ja-jp.yml (Japanese, Kansai)

* New translations ja-jp.yml (Korean (Gyeongsang))

* New translations ja-jp.yml (Chinese Simplified)

* New translations ja-jp.yml (Korean)

* New translations ja-jp.yml (Korean)
---
 locales/ar-SA.yml |  3 ---
 locales/bn-BD.yml |  3 ---
 locales/ca-ES.yml |  6 +++---
 locales/cs-CZ.yml |  3 ---
 locales/de-DE.yml |  3 ---
 locales/en-US.yml |  3 ---
 locales/es-ES.yml |  3 ---
 locales/fr-FR.yml |  3 ---
 locales/id-ID.yml |  3 ---
 locales/it-IT.yml |  8 +++++---
 locales/ja-KS.yml |  3 ---
 locales/ko-GS.yml |  2 --
 locales/ko-KR.yml | 23 ++++++++++++++++-------
 locales/pl-PL.yml |  3 ---
 locales/pt-PT.yml |  3 ---
 locales/ro-RO.yml |  3 ---
 locales/ru-RU.yml |  3 ---
 locales/sk-SK.yml |  3 ---
 locales/th-TH.yml |  3 ---
 locales/uk-UA.yml |  3 ---
 locales/uz-UZ.yml |  3 ---
 locales/vi-VN.yml |  3 ---
 locales/zh-CN.yml | 10 +++++++---
 locales/zh-TW.yml |  4 +---
 24 files changed, 32 insertions(+), 75 deletions(-)

diff --git a/locales/ar-SA.yml b/locales/ar-SA.yml
index 24b15ee693..d95600cb1f 100644
--- a/locales/ar-SA.yml
+++ b/locales/ar-SA.yml
@@ -626,10 +626,7 @@ abuseReported: "أُرسل البلاغ، شكرًا لك"
 reporter: "المُبلّغ"
 reporteeOrigin: "أصل البلاغ"
 reporterOrigin: "أصل المُبلّغ"
-forwardReport: "وجّه البلاغ إلى المثيل البعيد"
-forwardReportIsAnonymous: "في المثيل البعيد سيظهر المبلّغ كحساب مجهول."
 send: "أرسل"
-abuseMarkAsResolved: "علّم البلاغ كمحلول"
 openInNewTab: "افتح في لسان جديد"
 defaultNavigationBehaviour: "سلوك الملاحة الافتراضي"
 editTheseSettingsMayBreakAccount: "تعديل هذه الإعدادات قد يسبب عطبًا لحسابك"
diff --git a/locales/bn-BD.yml b/locales/bn-BD.yml
index 642fdf2b73..ab0ee74bb4 100644
--- a/locales/bn-BD.yml
+++ b/locales/bn-BD.yml
@@ -624,10 +624,7 @@ abuseReported: "আপনার অভিযোগটি দাখিল কর
 reporter: "অভিযোগকারী"
 reporteeOrigin: "অভিযোগটির উৎস"
 reporterOrigin: "অভিযোগকারীর উৎস"
-forwardReport: "রিমোট ইন্সত্যান্সে অভিযোগটি পাঠান"
-forwardReportIsAnonymous: "আপনার তথ্য রিমোট ইন্সত্যান্সে পাঠানো হবে না এবং একটি বেনামী সিস্টেম অ্যাকাউন্ট হিসাবে প্রদর্শিত হবে।"
 send: "পাঠান"
-abuseMarkAsResolved: "অভিযোগটিকে সমাধাকৃত হিসাবে চিহ্নিত করুন"
 openInNewTab: "নতুন ট্যাবে খুলুন"
 openInSideView: "সাইড ভিউতে খুলুন"
 defaultNavigationBehaviour: "ডিফল্ট নেভিগেশন"
diff --git a/locales/ca-ES.yml b/locales/ca-ES.yml
index bcea736e7a..ad5fde37bc 100644
--- a/locales/ca-ES.yml
+++ b/locales/ca-ES.yml
@@ -8,6 +8,8 @@ search: "Cercar"
 notifications: "Notificacions"
 username: "Nom d'usuari"
 password: "Contrasenya"
+initialPasswordForSetup: "Contrasenya inicial per la configuració inicial"
+initialPasswordIsIncorrect: "La contrasenya no és correcta."
 forgotPassword: "Contrasenya oblidada"
 fetchingAsApObject: "Cercant en el Fediverse..."
 ok: "OK"
@@ -716,10 +718,7 @@ abuseReported: "La teva denúncia s'ha enviat. Moltes gràcies."
 reporter: "Denunciant "
 reporteeOrigin: "Origen de la denúncia "
 reporterOrigin: "Origen del denunciant"
-forwardReport: "Transferir la denúncia a una instància remota"
-forwardReportIsAnonymous: "En lloc del teu compte, es farà servir un compte anònim com a denunciant al servidor remot."
 send: "Envia"
-abuseMarkAsResolved: "Marca la denúncia com a resolta"
 openInNewTab: "Obre a una pestanya nova"
 openInSideView: "Obre a una vista lateral"
 defaultNavigationBehaviour: "Navegació per defecte"
@@ -921,6 +920,7 @@ followersVisibility: "Visibilitat dels seguidors"
 continueThread: "Veure la continuació del fil"
 deleteAccountConfirm: "Això eliminarà el teu compte irreversiblement. Procedir?"
 incorrectPassword: "Contrasenya incorrecta."
+incorrectTotp: "La contrasenya no és correcta, o ha caducat."
 voteConfirm: "Confirma el teu vot \"{choice}\""
 hide: "Amagar"
 useDrawerReactionPickerForMobile: "Mostrar el selector de reaccions com un calaix al mòbil "
diff --git a/locales/cs-CZ.yml b/locales/cs-CZ.yml
index 1e391fcc31..4233a68f17 100644
--- a/locales/cs-CZ.yml
+++ b/locales/cs-CZ.yml
@@ -657,10 +657,7 @@ abuseReported: "Nahlášení bylo odesláno. Děkujeme převelice."
 reporter: "Nahlásil"
 reporteeOrigin: "Původ nahlášení"
 reporterOrigin: "Původ nahlasovače"
-forwardReport: "Přeposlat nahlášení do vzdálené instance"
-forwardReportIsAnonymous: "Místo vašeho účtu se ve vzdálené instanci zobrazí anonymní systémový účet jako nahlašovač."
 send: "Odeslat"
-abuseMarkAsResolved: "Označit nahlášení jako vyřešené"
 openInNewTab: "Otevřít v nové kartě"
 openInSideView: "Otevřít v bočním panelu"
 defaultNavigationBehaviour: "Výchozí chování navigace"
diff --git a/locales/de-DE.yml b/locales/de-DE.yml
index 871ed87564..35a04b453c 100644
--- a/locales/de-DE.yml
+++ b/locales/de-DE.yml
@@ -686,10 +686,7 @@ abuseReported: "Deine Meldung wurde versendet. Vielen Dank."
 reporter: "Melder"
 reporteeOrigin: "Herkunft des Gemeldeten"
 reporterOrigin: "Herkunft des Meldenden"
-forwardReport: "Meldung an fremde Instanz weiterleiten"
-forwardReportIsAnonymous: "Anstatt deines Benutzerkontos wird bei der fremden Instanz ein anonymes Systemkonto als Melder angezeigt."
 send: "Senden"
-abuseMarkAsResolved: "Meldung als gelöst markieren"
 openInNewTab: "In neuem Tab öffnen"
 openInSideView: "In Seitenansicht öffnen"
 defaultNavigationBehaviour: "Standardnavigationsverhalten"
diff --git a/locales/en-US.yml b/locales/en-US.yml
index 7af6d65ea4..7b275c990c 100644
--- a/locales/en-US.yml
+++ b/locales/en-US.yml
@@ -719,10 +719,7 @@ abuseReported: "Your report has been sent. Thank you very much."
 reporter: "Reporter"
 reporteeOrigin: "Reportee Origin"
 reporterOrigin: "Reporter Origin"
-forwardReport: "Forward report to remote instance"
-forwardReportIsAnonymous: "Instead of your account, an anonymous system account will be displayed as reporter at the remote instance."
 send: "Send"
-abuseMarkAsResolved: "Mark report as resolved"
 openInNewTab: "Open in new tab"
 openInSideView: "Open in side view"
 defaultNavigationBehaviour: "Default navigation behavior"
diff --git a/locales/es-ES.yml b/locales/es-ES.yml
index 10966a77b6..de9ea0c32a 100644
--- a/locales/es-ES.yml
+++ b/locales/es-ES.yml
@@ -700,10 +700,7 @@ abuseReported: "Se ha enviado el reporte. Muchas gracias."
 reporter: "Reportador"
 reporteeOrigin: "Reportar a"
 reporterOrigin: "Origen del reporte"
-forwardReport: "Transferir un informe a una instancia remota"
-forwardReportIsAnonymous: "No puede ver su información de la instancia remota y aparecerá como una cuenta anónima del sistema"
 send: "Enviar"
-abuseMarkAsResolved: "Marcar reporte como resuelto"
 openInNewTab: "Abrir en una Nueva Pestaña"
 openInSideView: "Abrir en una vista al costado"
 defaultNavigationBehaviour: "Navegación por defecto"
diff --git a/locales/fr-FR.yml b/locales/fr-FR.yml
index d15fadcb1c..7dfc64d63f 100644
--- a/locales/fr-FR.yml
+++ b/locales/fr-FR.yml
@@ -691,10 +691,7 @@ abuseReported: "Le rapport est envoyé. Merci."
 reporter: "Signalé par"
 reporteeOrigin: "Origine du signalement"
 reporterOrigin: "Signalé par"
-forwardReport: "Transférer le signalement à l’instance distante"
-forwardReportIsAnonymous: "L'instance distante ne sera pas en mesure de voir vos informations et apparaîtra comme un compte anonyme du système."
 send: "Envoyer"
-abuseMarkAsResolved: "Marquer le signalement comme résolu"
 openInNewTab: "Ouvrir dans un nouvel onglet"
 openInSideView: "Ouvrir en vue latérale"
 defaultNavigationBehaviour: "Navigation par défaut"
diff --git a/locales/id-ID.yml b/locales/id-ID.yml
index 4c2040dd07..fbfedb89e3 100644
--- a/locales/id-ID.yml
+++ b/locales/id-ID.yml
@@ -702,10 +702,7 @@ abuseReported: "Laporan kamu telah dikirimkan. Terima kasih."
 reporter: "Pelapor"
 reporteeOrigin: "Yang dilaporkan"
 reporterOrigin: "Pelapor"
-forwardReport: "Teruskan laporan ke instansi luar"
-forwardReportIsAnonymous: "Untuk melindungi privasi akun kamu, akun anonim dari sistem akan digunakan sebagai pelapor pada instansi luar."
 send: "Kirim"
-abuseMarkAsResolved: "Tandai laporan sebagai selesai"
 openInNewTab: "Buka di tab baru"
 openInSideView: "Buka di tampilan samping"
 defaultNavigationBehaviour: "Navigasi bawaan"
diff --git a/locales/it-IT.yml b/locales/it-IT.yml
index 0399ba4d9c..004bb6e9fd 100644
--- a/locales/it-IT.yml
+++ b/locales/it-IT.yml
@@ -8,6 +8,9 @@ search: "Cerca"
 notifications: "Notifiche"
 username: "Nome utente"
 password: "Password"
+initialPasswordForSetup: "Password iniziale, per avviare le impostazioni"
+initialPasswordIsIncorrect: "Password iniziale, sbagliata."
+initialPasswordForSetupDescription: "Se hai installato Misskey di persona, usa la password che hai indicato nel file di configurazione.\nSe stai utilizzando un servizio di hosting Misskey, usa la password fornita dal gestore.\nSe non hai una password preimpostata, lascia il campo vuoto e continua."
 forgotPassword: "Hai dimenticato la password?"
 fetchingAsApObject: "Recuperando dal Fediverso..."
 ok: "OK"
@@ -716,10 +719,7 @@ abuseReported: "La segnalazione è stata inviata. Grazie."
 reporter: "il corrispondente"
 reporteeOrigin: "Segnalazione a"
 reporterOrigin: "Segnalazione da"
-forwardReport: "Inoltro di un report a un'istanza remota."
-forwardReportIsAnonymous: "L'istanza remota non vedrà le tue informazioni, apparirai come profilo di sistema, anonimo."
 send: "Inviare"
-abuseMarkAsResolved: "Risolvi segnalazione"
 openInNewTab: "Apri in una nuova scheda"
 openInSideView: "Apri in vista laterale"
 defaultNavigationBehaviour: "Navigazione preimpostata"
@@ -921,6 +921,7 @@ followersVisibility: "Visibilità dei profili che ti seguono"
 continueThread: "Altre conversazioni"
 deleteAccountConfirm: "Così verrà eliminato il profilo. Vuoi procedere?"
 incorrectPassword: "La password è errata."
+incorrectTotp: "Il codice OTP è sbagliato, oppure scaduto."
 voteConfirm: "Votare per「{choice}」?"
 hide: "Nascondere"
 useDrawerReactionPickerForMobile: "Mostra sul drawer da dispositivo mobile"
@@ -2393,6 +2394,7 @@ _notification:
   followedBySomeUsers: "{n} follower"
   flushNotification: "Azzera le notifiche"
   exportOfXCompleted: "Abbiamo completato l'esportazione di {x}"
+  login: "Autenticazione avvenuta"
   _types:
     all: "Tutto"
     note: "Nuove Note"
diff --git a/locales/ja-KS.yml b/locales/ja-KS.yml
index 4f950059a7..52a8f41380 100644
--- a/locales/ja-KS.yml
+++ b/locales/ja-KS.yml
@@ -707,10 +707,7 @@ abuseReported: "無事内容が送信されたみたいやで。おおきに〜
 reporter: "通報者"
 reporteeOrigin: "通報先"
 reporterOrigin: "通報元"
-forwardReport: "リモートサーバーに通報を転送するで"
-forwardReportIsAnonymous: "リモートサーバーからはあんたの情報は見えんなって、匿名のシステムアカウントとして表示されるで。"
 send: "送信"
-abuseMarkAsResolved: "対応したで"
 openInNewTab: "新しいタブで開く"
 openInSideView: "サイドビューで開く"
 defaultNavigationBehaviour: "デフォルトのナビゲーション"
diff --git a/locales/ko-GS.yml b/locales/ko-GS.yml
index f8a0d328a3..6c667b48da 100644
--- a/locales/ko-GS.yml
+++ b/locales/ko-GS.yml
@@ -601,8 +601,6 @@ reportAbuseOf: "{name}님얼 신고하기"
 reporter: "신고한 사람"
 reporteeOrigin: "신고덴 사람"
 reporterOrigin: "신고한 곳"
-forwardReport: "웬겍 서버에 신고 보내기"
-forwardReportIsAnonymous: "웬겍 서버서는 나으 정보럴 몬 보고 익멩으 시스템 게정어로 보입니다."
 waitingFor: "{x}(얼)럴 지달리고 잇십니다"
 random: "무작이"
 system: "시스템"
diff --git a/locales/ko-KR.yml b/locales/ko-KR.yml
index b85bc048e1..757afe53f9 100644
--- a/locales/ko-KR.yml
+++ b/locales/ko-KR.yml
@@ -454,6 +454,7 @@ totpDescription: "인증 앱을 사용하여 일회성 비밀번호 입력"
 moderator: "모더레이터"
 moderation: "조정"
 moderationNote: "조정 기록"
+moderationNoteDescription: "모더레이터 역할을 가진 유저만 보이는 메모를 적을 수 있습니다."
 addModerationNote: "조정 기록 추가하기"
 moderationLogs: "모더레이션 로그"
 nUsersMentioned: "{n}명이 언급함"
@@ -719,10 +720,7 @@ abuseReported: "신고를 보냈습니다. 신고해 주셔서 감사합니다."
 reporter: "신고자"
 reporteeOrigin: "피신고자"
 reporterOrigin: "신고자"
-forwardReport: "리모트 서버에도 신고 내용 보내기"
-forwardReportIsAnonymous: "리모트 서버에서는 나의 정보를 볼 수 없으며, 익명의 시스템 계정으로 표시됩니다."
 send: "전송"
-abuseMarkAsResolved: "해결됨으로 표시"
 openInNewTab: "새 탭에서 열기"
 openInSideView: "사이드뷰로 열기"
 defaultNavigationBehaviour: "기본 탐색 동작"
@@ -924,6 +922,7 @@ followersVisibility: "팔로워의 공개 범위"
 continueThread: "글타래 더 보기"
 deleteAccountConfirm: "계정이 삭제되고 되돌릴 수 없게 됩니다. 계속하시겠습니까? "
 incorrectPassword: "비밀번호가 올바르지 않습니다."
+incorrectTotp: "OTP 번호가 틀렸거나 유효기간이 만료되어 있을 수 있습니다."
 voteConfirm: "\"{choice}\"에 투표하시겠습니까?"
 hide: "숨기기"
 useDrawerReactionPickerForMobile: "모바일에서 드로어 메뉴로 표시"
@@ -1123,7 +1122,7 @@ preservedUsernames: "예약한 사용자 이름"
 preservedUsernamesDescription: "예약할 사용자명을 한 줄에 하나씩 입력합니다. 여기에서 지정한 사용자명으로는 계정을 생성할 수 없게 됩니다. 단, 관리자 권한으로 계정을 생성할 때에는 해당되지 않으며, 이미 존재하는 계정도 영향을 받지 않습니다."
 createNoteFromTheFile: "이 파일로 노트를 작성"
 archive: "아카이브"
-archived: "보관됨"
+archived: "아카이브 됨"
 unarchive: "보관 취소"
 channelArchiveConfirmTitle: "{name} 채널을 보존하시겠습니까?"
 channelArchiveConfirmDescription: "보존한 채널은 채널 목록과 검색 결과에 표시되지 않으며 새로운 노트도 작성할 수 없습니다."
@@ -1287,6 +1286,14 @@ unknownWebAuthnKey: "등록되지 않은 패스키입니다."
 passkeyVerificationFailed: "패스키 검증을 실패했습니다."
 passkeyVerificationSucceededButPasswordlessLoginDisabled: "패스키를 검증했으나, 비밀번호 없이 로그인하기가 꺼져 있습니다."
 messageToFollower: "팔로워에 보낼 메시지"
+target: "대상"
+_abuseUserReport:
+  forward: "전달"
+  forwardDescription: "익명 시스템 계정을 사용하여 리모트 서버에 신고 내용을 전달할 수 있습니다."
+  resolve: "해결됨"
+  accept: "인용"
+  reject: "기각"
+  resolveTutorial: "적절한 신고 내용에 대응한 경우, \"인용\"을 선택하여 \"해결됨\"으로 기록합니다.\n적절하지 않은 신고를 받은 경우, \"기각\"을 선택하여 \"기각\"으로 기록합니다."
 _delivery:
   status: "전송 상태"
   stop: "정지됨"
@@ -1993,7 +2000,7 @@ _sfx:
 _soundSettings:
   driveFile: "드라이브에 있는 오디오를 사용"
   driveFileWarn: "드라이브에 있는 파일을 선택하세요."
-  driveFileTypeWarn: "이 파일은 지원되지 않습니다."
+  driveFileTypeWarn: "이 파이"
   driveFileTypeWarnDescription: "오디오 파일을 선택하세요."
   driveFileDurationWarn: "오디오가 너무 깁니다"
   driveFileDurationWarnDescription: "긴 오디오로 설정할 경우 미스키 사용에 지장이 갈 수도 있습니다. 그래도 괜찮습니까?"
@@ -2476,7 +2483,7 @@ _webhookSettings:
     reaction: "누군가 내 노트에 리액션했을 때"
     mention: "누군가 나를 멘션했을 때"
   _systemEvents:
-    abuseReport: "유저로부터 신고를 받았을 때"
+    abuseReport: "유저롭"
     abuseReportResolved: "받은 신고를 처리했을 때"
     userCreated: "유저가 생성되었을 때"
   deleteConfirm: "Webhook을 삭제할까요?"
@@ -2524,6 +2531,8 @@ _moderationLogTypes:
   markSensitiveDriveFile: "파일에 열람주의를 설정"
   unmarkSensitiveDriveFile: "파일에 열람주의를 해제"
   resolveAbuseReport: "신고 처리"
+  forwardAbuseReport: "신고 전달"
+  updateAbuseReportNote: "신고 조정 노트 갱신"
   createInvitation: "초대 코드 생성"
   createAd: "광고 생성"
   deleteAd: "광고 삭제"
@@ -2663,7 +2672,7 @@ _urlPreviewSetting:
   timeoutDescription: "미리보기를 로딩하는데 걸리는 시간이 정한 시간보다 오래 걸리는 경우, 미리보기를 생성하지 않습니다."
   maximumContentLength: "Content-Length의 최대치 (byte)"
   maximumContentLengthDescription: "Content-Length가 이 값을 넘어서면 미리보기를 생성하지 않습니다."
-  requireContentLength: "Content-Length를 얻었을 때만 미리보기 만들기"
+  requireContentLength: "Content-Length를 받아온 경우에만 "
   requireContentLengthDescription: "상대 서버가 Content-Length를 되돌려주지 않는다면 미리보기를 만들지 않습니다."
   userAgent: "User-Agent"
   userAgentDescription: "미리보기를 얻을 때 사용한 User-Agent를 설정합니다. 비어 있다면 기본값의 User-Agent를 사용합니다."
diff --git a/locales/pl-PL.yml b/locales/pl-PL.yml
index 0073628673..117434ad32 100644
--- a/locales/pl-PL.yml
+++ b/locales/pl-PL.yml
@@ -689,10 +689,7 @@ abuseReported: "Twoje zgłoszenie zostało wysłane. Dziękujemy."
 reporter: "Zgłaszający"
 reporteeOrigin: "Pochodzenie zgłoszonego"
 reporterOrigin: "Pochodzenie zgłaszającego"
-forwardReport: "Przekaż zgłoszenie do innej instancji"
-forwardReportIsAnonymous: "Zamiast twojego konta, anonimowe konto systemowe będzie wyświetlone jako zgłaszający na instancji zdalnej."
 send: "Wyślij"
-abuseMarkAsResolved: "Oznacz zgłoszenie jako rozwiązane"
 openInNewTab: "Otwórz w nowej karcie"
 openInSideView: "Otwórz w bocznym widoku"
 defaultNavigationBehaviour: "Domyślne zachowanie nawigacji"
diff --git a/locales/pt-PT.yml b/locales/pt-PT.yml
index f5d29891df..dac4abbe64 100644
--- a/locales/pt-PT.yml
+++ b/locales/pt-PT.yml
@@ -707,10 +707,7 @@ abuseReported: "Denúncia enviada. Obrigado por sua ajuda."
 reporter: "Denunciante"
 reporteeOrigin: "Origem da denúncia"
 reporterOrigin: "Origem do denunciante"
-forwardReport: "Encaminhar a denúncia para o servidor remoto"
-forwardReportIsAnonymous: "No servidor remoto, suas informações não serão visíveis, e você será apresentado como uma conta do sistema anônima."
 send: "Enviar"
-abuseMarkAsResolved: "Marcar denúncia como resolvida"
 openInNewTab: "Abrir em nova aba"
 openInSideView: "Abrir em visão lateral"
 defaultNavigationBehaviour: "Navegação padrão"
diff --git a/locales/ro-RO.yml b/locales/ro-RO.yml
index 88495a41a1..3cc09aa5c2 100644
--- a/locales/ro-RO.yml
+++ b/locales/ro-RO.yml
@@ -625,10 +625,7 @@ abuseReported: "Raportul tău a fost trimis. Mulțumim."
 reporter: "Raportorul"
 reporteeOrigin: "Originea raportatului"
 reporterOrigin: "Originea raportorului"
-forwardReport: "Redirecționează raportul către instanța externă"
-forwardReportIsAnonymous: "În locul contului tău, va fi afișat un cont anonim, de sistem, ca raportor către instanța externă."
 send: "Trimite"
-abuseMarkAsResolved: "Marchează raportul ca rezolvat"
 openInNewTab: "Deschide în tab nou"
 openInSideView: "Deschide în vedere laterală"
 defaultNavigationBehaviour: "Comportament de navigare implicit"
diff --git a/locales/ru-RU.yml b/locales/ru-RU.yml
index 15e33c7f4d..befb537105 100644
--- a/locales/ru-RU.yml
+++ b/locales/ru-RU.yml
@@ -700,10 +700,7 @@ abuseReported: "Жалоба отправлена. Большое спасибо
 reporter: "Сообщивший"
 reporteeOrigin: "О ком сообщено"
 reporterOrigin: "Кто сообщил"
-forwardReport: "Отправить жалобу на инстанс автора."
-forwardReportIsAnonymous: "Жалоба на удалённый инстанс будет отправлена анонимно. Вместо ваших данных у получателя будет отображена системная учётная запись."
 send: "Отправить"
-abuseMarkAsResolved: "Отметить жалобу как решённую"
 openInNewTab: "Открыть в новой вкладке"
 openInSideView: "Открывать в боковой колонке"
 defaultNavigationBehaviour: "Поведение навигации по умолчанию"
diff --git a/locales/sk-SK.yml b/locales/sk-SK.yml
index ad004eb4e2..8cb73e1303 100644
--- a/locales/sk-SK.yml
+++ b/locales/sk-SK.yml
@@ -631,10 +631,7 @@ abuseReported: "Vaše nahlásenie je odoslané. Veľmi pekne ďakujeme."
 reporter: "Nahlásil"
 reporteeOrigin: "Pôvod nahláseného"
 reporterOrigin: "Pôvod nahlasovača"
-forwardReport: "Preposlať nahlásenie na server"
-forwardReportIsAnonymous: "Namiesto vášho účtu bude zobrazený anonymný systémový účet na vzdialenom serveri ako autor nahlásenia."
 send: "Poslať"
-abuseMarkAsResolved: "Označiť nahlásenia ako vyriešené"
 openInNewTab: "Otvoriť v novom tabe"
 openInSideView: "Otvoriť v bočnom paneli"
 defaultNavigationBehaviour: "Predvolené správanie navigácie"
diff --git a/locales/th-TH.yml b/locales/th-TH.yml
index 77fea6a68e..31eee2bccc 100644
--- a/locales/th-TH.yml
+++ b/locales/th-TH.yml
@@ -707,10 +707,7 @@ abuseReported: "เราได้ส่งรายงานของคุณ
 reporter: "ผู้รายงาน"
 reporteeOrigin: "ปลายทางรายงาน"
 reporterOrigin: "แหล่งผู้รายงาน"
-forwardReport: "ส่งต่อรายงานไปยังเซิร์ฟเวอร์ระยะไกล"
-forwardReportIsAnonymous: "ข้อมูลของคุณจะไม่ปรากฏบนเซิร์ฟเวอร์ระยะไกลและปรากฏเป็นบัญชีระบบที่ไม่ระบุชื่อ"
 send: "ส่ง"
-abuseMarkAsResolved: "ทำเครื่องหมายรายงานว่าแก้ไขแล้ว"
 openInNewTab: "เปิดในแท็บใหม่"
 openInSideView: "เปิดในมุมมองด้านข้าง"
 defaultNavigationBehaviour: "พฤติกรรมการนำทางที่เป็นค่าเริ่มต้น"
diff --git a/locales/uk-UA.yml b/locales/uk-UA.yml
index ef01c8186c..974508b3a7 100644
--- a/locales/uk-UA.yml
+++ b/locales/uk-UA.yml
@@ -630,10 +630,7 @@ abuseReported: "Дякуємо, вашу скаргу було відправл
 reporter: "Репортер"
 reporteeOrigin: "Про кого повідомлено"
 reporterOrigin: "Хто повідомив"
-forwardReport: "Переслати звіт на віддалений інстанс"
-forwardReportIsAnonymous: "Замість вашого облікового запису анонімний системний обліковий запис буде відображатися як доповідач на віддаленому інстансі"
 send: "Відправити"
-abuseMarkAsResolved: "Позначити скаргу як вирішену"
 openInNewTab: "Відкрити в новій вкладці"
 openInSideView: "Відкрити збоку"
 defaultNavigationBehaviour: "Поведінка навігації за замовчуванням"
diff --git a/locales/uz-UZ.yml b/locales/uz-UZ.yml
index 7c5d2796f6..37a550008a 100644
--- a/locales/uz-UZ.yml
+++ b/locales/uz-UZ.yml
@@ -629,10 +629,7 @@ abuseReported: "Shikoyatingiz yetkazildi. Ma'lumot uchun rahmat."
 reporter: "Shikoyat qiluvchi"
 reporteeOrigin: "Xabarning kelib chiqishi"
 reporterOrigin: "Xabarchining joylashuvi"
-forwardReport: "Xabarni masofadagi serverga yuborish"
-forwardReportIsAnonymous: "Sizning yuborayotgan xabaringiz o'z akkountingiz emas balki anonim tarzda qoladi"
 send: "Yuborish"
-abuseMarkAsResolved: "Yuborilgan xabarni hal qilingan deb belgilash"
 openInNewTab: "Yangi tab da ochish"
 openInSideView: "Yon panelda ochish"
 defaultNavigationBehaviour: "Standart navigatsiya harakati"
diff --git a/locales/vi-VN.yml b/locales/vi-VN.yml
index c84eb574f3..6cf9b3f278 100644
--- a/locales/vi-VN.yml
+++ b/locales/vi-VN.yml
@@ -675,10 +675,7 @@ abuseReported: "Báo cáo đã được gửi. Cảm ơn bạn nhiều."
 reporter: "Người báo cáo"
 reporteeOrigin: "Bị báo cáo"
 reporterOrigin: "Máy chủ người báo cáo"
-forwardReport: "Chuyển tiếp báo cáo cho máy chủ từ xa"
-forwardReportIsAnonymous: "Thay vì tài khoản của bạn, một tài khoản hệ thống ẩn danh sẽ được hiển thị dưới dạng người báo cáo ở máy chủ từ xa."
 send: "Gửi"
-abuseMarkAsResolved: "Đánh dấu đã xử lý"
 openInNewTab: "Mở trong tab mới"
 openInSideView: "Mở trong thanh bên"
 defaultNavigationBehaviour: "Thao tác điều hướng mặc định"
diff --git a/locales/zh-CN.yml b/locales/zh-CN.yml
index 15f84e845d..c4bfe972fe 100644
--- a/locales/zh-CN.yml
+++ b/locales/zh-CN.yml
@@ -454,6 +454,7 @@ totpDescription: "使用验证器输入一次性密码"
 moderator: "监察员"
 moderation: "管理"
 moderationNote: "管理笔记"
+moderationNoteDescription: "可以用来记录仅在管理员之间共享的笔记。"
 addModerationNote: "添加管理笔记"
 moderationLogs: "管理日志"
 nUsersMentioned: "{n} 被提到"
@@ -719,10 +720,7 @@ abuseReported: "内容已发送。感谢您提交信息。"
 reporter: "举报者"
 reporteeOrigin: "举报来源"
 reporterOrigin: "举报者来源"
-forwardReport: "将该举报信息转发给远程服务器"
-forwardReportIsAnonymous: "在远程实例上显示的报告者是匿名的系统账号,而不是您的账号。"
 send: "发送"
-abuseMarkAsResolved: "处理完毕"
 openInNewTab: "在新标签页中打开"
 openInSideView: "在侧边栏中打开"
 defaultNavigationBehaviour: "默认导航"
@@ -1288,6 +1286,10 @@ unknownWebAuthnKey: "此通行密钥未注册。"
 passkeyVerificationFailed: "验证通行密钥失败。"
 passkeyVerificationSucceededButPasswordlessLoginDisabled: "通行密钥验证成功,但账户未开启无密码登录。"
 messageToFollower: "给关注者的消息"
+target: "对象"
+_abuseUserReport:
+  forward: "转发"
+  forwardDescription: "目标是匿名系统账户,将把举报转发给远程服务器。"
 _delivery:
   status: "投递状态"
   stop: "停止投递"
@@ -2525,6 +2527,8 @@ _moderationLogTypes:
   markSensitiveDriveFile: "标记网盘文件为敏感媒体"
   unmarkSensitiveDriveFile: "取消标记网盘文件为敏感媒体"
   resolveAbuseReport: "处理举报"
+  forwardAbuseReport: "转发举报"
+  updateAbuseReportNote: "更新举报用管理笔记"
   createInvitation: "生成邀请码"
   createAd: "创建了广告"
   deleteAd: "删除了广告"
diff --git a/locales/zh-TW.yml b/locales/zh-TW.yml
index 6659efcb7a..5e8a5d8f8d 100644
--- a/locales/zh-TW.yml
+++ b/locales/zh-TW.yml
@@ -719,10 +719,7 @@ abuseReported: "檢舉完成。感謝您的報告。"
 reporter: "檢舉者"
 reporteeOrigin: "檢舉來源"
 reporterOrigin: "檢舉者來源"
-forwardReport: "將報告轉送給遠端伺服器"
-forwardReportIsAnonymous: "在遠端實例上看不到您的資訊,顯示的報告者是匿名的系统帳戶。"
 send: "發送"
-abuseMarkAsResolved: "處理完畢"
 openInNewTab: "在新分頁中開啟"
 openInSideView: "在側欄中開啟"
 defaultNavigationBehaviour: "預設導航"
@@ -924,6 +921,7 @@ followersVisibility: "追隨者的可見性"
 continueThread: "查看更多貼文"
 deleteAccountConfirm: "將要刪除帳戶。是否確定?"
 incorrectPassword: "密碼錯誤。"
+incorrectTotp: "一次性密碼錯誤,或者已過期。"
 voteConfirm: "確定投給「{choice}」?"
 hide: "隱藏"
 useDrawerReactionPickerForMobile: "在移動設備上使用抽屜顯示"

From ed89b4bd94fae695959b006122603bb28b94dbc9 Mon Sep 17 00:00:00 2001
From: FineArchs <133759614+FineArchs@users.noreply.github.com>
Date: Mon, 7 Oct 2024 09:46:04 +0900
Subject: [PATCH 058/121] =?UTF-8?q?refactor:=20=E6=8B=A1=E5=BC=B5=E6=A9=9F?=
 =?UTF-8?q?=E8=83=BD=E3=82=A4=E3=83=B3=E3=82=B9=E3=83=88=E3=83=BC=E3=83=AB?=
 =?UTF-8?q?=E3=81=AE=E3=83=9A=E3=83=BC=E3=82=B8=E3=81=AE=E4=B8=80=E9=83=A8?=
 =?UTF-8?q?=E3=82=92=E3=82=B3=E3=83=B3=E3=83=9D=E3=83=BC=E3=83=8D=E3=83=B3?=
 =?UTF-8?q?=E3=83=88=E3=81=A8=E3=81=97=E3=81=A6=E5=88=86=E9=9B=A2=20(#1465?=
 =?UTF-8?q?4)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

* create MkExtensionInstaller.vue

* annotation

* add fallbacks

* storybook

* update annotations

* Update MkExtensionInstaller.vue

* use additonalInfo slot
---
 .../MkExtensionInstaller.stories.impl.ts      |  83 ++++++++++
 .../src/components/MkExtensionInstaller.vue   | 146 ++++++++++++++++++
 .../frontend/src/pages/install-extensions.vue | 117 +++-----------
 3 files changed, 250 insertions(+), 96 deletions(-)
 create mode 100644 packages/frontend/src/components/MkExtensionInstaller.stories.impl.ts
 create mode 100644 packages/frontend/src/components/MkExtensionInstaller.vue

diff --git a/packages/frontend/src/components/MkExtensionInstaller.stories.impl.ts b/packages/frontend/src/components/MkExtensionInstaller.stories.impl.ts
new file mode 100644
index 0000000000..6763f7c546
--- /dev/null
+++ b/packages/frontend/src/components/MkExtensionInstaller.stories.impl.ts
@@ -0,0 +1,83 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { StoryObj } from '@storybook/vue3';
+import MkExtensionInstaller from './MkExtensionInstaller.vue';
+import lightTheme from '@@/themes/_light.json5';
+
+export const Plugin = {
+	render(args) {
+		return {
+			components: {
+				MkExtensionInstaller,
+			},
+			setup() {
+				return {
+					args,
+				};
+			},
+			computed: {
+				props() {
+					return {
+						...this.args,
+					};
+				},
+			},
+			template: '<MkExtensionInstaller v-bind="props" />',
+		};
+	},
+	args: {
+		extension: {
+			type: 'plugin',
+			raw: '"do nothing"',
+			meta: {
+				name: 'do nothing plugin',
+				version: '1.0',
+				author: 'syuilo and misskey-project',
+				description: 'a plugin that does nothing',
+				permissions: ['read:account'],
+				config: {
+					'doNothing': true,
+				},
+			},
+		},
+	},
+	parameters: {
+		layout: 'centered',
+	},
+} satisfies StoryObj<typeof MkExtensionInstaller>;
+
+export const Theme = {
+	render(args) {
+		return {
+			components: {
+				MkExtensionInstaller,
+			},
+			setup() {
+				return {
+					args,
+				};
+			},
+			computed: {
+				props() {
+					return {
+						...this.args,
+					};
+				},
+			},
+			template: '<MkExtensionInstaller v-bind="props" />',
+		};
+	},
+	args: {
+		extension: {
+			type: 'theme',
+			raw: JSON.stringify(lightTheme),
+			meta: lightTheme,
+		},
+	},
+	parameters: {
+		layout: 'centered',
+	},
+} satisfies StoryObj<typeof MkExtensionInstaller>;
diff --git a/packages/frontend/src/components/MkExtensionInstaller.vue b/packages/frontend/src/components/MkExtensionInstaller.vue
new file mode 100644
index 0000000000..0f7acd69e7
--- /dev/null
+++ b/packages/frontend/src/components/MkExtensionInstaller.vue
@@ -0,0 +1,146 @@
+<!--
+SPDX-FileCopyrightText: syuilo and misskey-project
+SPDX-License-Identifier: AGPL-3.0-only
+-->
+
+<template>
+<div class="_gaps_m" :class="$style.extInstallerRoot">
+	<div :class="$style.extInstallerIconWrapper">
+		<i v-if="isPlugin" class="ti ti-plug"></i>
+		<i v-else-if="isTheme" class="ti ti-palette"></i>
+		<!-- 拡張用? -->
+		<i v-else class="ti ti-download"></i>
+	</div>
+	<h2 :class="$style.extInstallerTitle">{{ i18n.ts._externalResourceInstaller[`_${extension.type}`].title }}</h2>
+	<div :class="$style.extInstallerNormDesc">{{ i18n.ts._externalResourceInstaller.checkVendorBeforeInstall }}</div>
+	<MkInfo v-if="isPlugin" :warn="true">{{ i18n.ts._plugin.installWarn }}</MkInfo>
+	<FormSection>
+		<template #label>{{ i18n.ts._externalResourceInstaller[`_${extension.type}`].metaTitle }}</template>
+		<div class="_gaps_s">
+			<FormSplit>
+				<MkKeyValue>
+					<template #key>{{ i18n.ts.name }}</template>
+					<template #value>{{ extension.meta.name }}</template>
+				</MkKeyValue>
+				<MkKeyValue>
+					<template #key>{{ i18n.ts.author }}</template>
+					<template #value>{{ extension.meta.author }}</template>
+				</MkKeyValue>
+			</FormSplit>
+			<MkKeyValue v-if="isPlugin">
+				<template #key>{{ i18n.ts.description }}</template>
+				<template #value>{{ extension.meta.description ?? i18n.ts.none }}</template>
+			</MkKeyValue>
+			<MkKeyValue v-if="isPlugin">
+				<template #key>{{ i18n.ts.version }}</template>
+				<template #value>{{ extension.meta.version }}</template>
+			</MkKeyValue>
+			<MkKeyValue v-if="isPlugin">
+				<template #key>{{ i18n.ts.permission }}</template>
+				<template #value>
+					<ul v-if="extension.meta.permissions && extension.meta.permissions.length > 0" :class="$style.extInstallerKVList">
+						<li v-for="permission in extension.meta.permissions" :key="permission">{{ i18n.ts._permissions[permission] }}</li>
+					</ul>
+					<template v-else>{{ i18n.ts.none }}</template>
+				</template>
+			</MkKeyValue>
+			<MkKeyValue v-if="isTheme">
+				<template #key>{{ i18n.ts._externalResourceInstaller._meta.base }}</template>
+				<template #value>{{ i18n.ts[extension.meta.base ?? 'none'] }}</template>
+			</MkKeyValue>
+			<MkFolder>
+				<template #icon><i class="ti ti-code"></i></template>
+				<template #label>{{ i18n.ts._plugin.viewSource }}</template>
+
+				<MkCode :code="extension.raw"/>
+			</MkFolder>
+		</div>
+	</FormSection>
+	<slot name="additionalInfo"/>
+	<div class="_buttonsCenter">
+		<MkButton primary @click="emits('confirm')"><i class="ti ti-check"></i> {{ i18n.ts.install }}</MkButton>
+	</div>
+</div>
+</template>
+
+<script lang="ts">
+export type Extension = {
+	type: 'plugin';
+	raw: string;
+	meta: {
+		name: string;
+		version: string;
+		author: string;
+		description?: string;
+		permissions?: string[];
+		config?: Record<string, any>;
+	};
+} | {
+	type: 'theme';
+	raw: string;
+	meta: {
+		name: string;
+		author: string;
+		base?: 'light' | 'dark';
+	};
+};
+</script>
+<script lang="ts" setup>
+import { computed } from 'vue';
+import MkButton from '@/components/MkButton.vue';
+import FormSection from '@/components/form/section.vue';
+import FormSplit from '@/components/form/split.vue';
+import MkCode from '@/components/MkCode.vue';
+import MkInfo from '@/components/MkInfo.vue';
+import MkFolder from '@/components/MkFolder.vue';
+import MkKeyValue from '@/components/MkKeyValue.vue';
+import { i18n } from '@/i18n.js';
+
+const isPlugin = computed(() => props.extension.type === 'plugin');
+const isTheme = computed(() => props.extension.type === 'theme');
+
+const props = defineProps<{
+	extension: Extension;
+}>();
+
+const emits = defineEmits<{
+	(ev: 'confirm'): void;
+}>();
+</script>
+
+<style lang="scss" module>
+.extInstallerRoot {
+	border-radius: var(--radius);
+	background: var(--panel);
+	padding: 1.5rem;
+}
+
+.extInstallerIconWrapper {
+	width: 48px;
+	height: 48px;
+	font-size: 24px;
+	line-height: 48px;
+	text-align: center;
+	border-radius: 50%;
+	margin-left: auto;
+	margin-right: auto;
+
+	background-color: var(--accentedBg);
+	color: var(--accent);
+}
+
+.extInstallerTitle {
+	font-size: 1.2rem;
+	text-align: center;
+	margin: 0;
+}
+
+.extInstallerNormDesc {
+	text-align: center;
+}
+
+.extInstallerKVList {
+	margin-top: 0;
+	margin-bottom: 0;
+}
+</style>
diff --git a/packages/frontend/src/pages/install-extensions.vue b/packages/frontend/src/pages/install-extensions.vue
index 4bee437f65..83f16fce68 100644
--- a/packages/frontend/src/pages/install-extensions.vue
+++ b/packages/frontend/src/pages/install-extensions.vue
@@ -8,76 +8,26 @@ SPDX-License-Identifier: AGPL-3.0-only
 	<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
 	<MkSpacer :contentMax="500">
 		<MkLoading v-if="uiPhase === 'fetching'"/>
-		<div v-else-if="uiPhase === 'confirm' && data" class="_gaps_m" :class="$style.extInstallerRoot">
-			<div :class="$style.extInstallerIconWrapper">
-				<i v-if="data.type === 'plugin'" class="ti ti-plug"></i>
-				<i v-else-if="data.type === 'theme'" class="ti ti-palette"></i>
-				<i v-else class="ti ti-download"></i>
-			</div>
-			<h2 :class="$style.extInstallerTitle">{{ i18n.ts._externalResourceInstaller[`_${data.type}`].title }}</h2>
-			<div :class="$style.extInstallerNormDesc">{{ i18n.ts._externalResourceInstaller.checkVendorBeforeInstall }}</div>
-			<MkInfo v-if="data.type === 'plugin'" :warn="true">{{ i18n.ts._plugin.installWarn }}</MkInfo>
-			<FormSection>
-				<template #label>{{ i18n.ts._externalResourceInstaller[`_${data.type}`].metaTitle }}</template>
-				<div class="_gaps_s">
-					<FormSplit>
+		<MkExtensionInstaller v-else-if="uiPhase === 'confirm' && data" :extension="data" @confirm="install()">
+			<template #additionalInfo>
+				<FormSection>
+					<template #label>{{ i18n.ts._externalResourceInstaller._vendorInfo.title }}</template>
+					<div class="_gaps_s">
 						<MkKeyValue>
-							<template #key>{{ i18n.ts.name }}</template>
-							<template #value>{{ data.meta?.name }}</template>
+							<template #key>{{ i18n.ts._externalResourceInstaller._vendorInfo.endpoint }}</template>
+							<template #value><MkUrl :url="url" :showUrlPreview="false"></MkUrl></template>
 						</MkKeyValue>
 						<MkKeyValue>
-							<template #key>{{ i18n.ts.author }}</template>
-							<template #value>{{ data.meta?.author }}</template>
+							<template #key>{{ i18n.ts._externalResourceInstaller._vendorInfo.hashVerify }}</template>
+							<template #value>
+								<!-- この画面が出ている時点でハッシュの検証には成功している -->
+								<i class="ti ti-check" style="color: var(--accent)"></i>
+							</template>
 						</MkKeyValue>
-					</FormSplit>
-					<MkKeyValue v-if="data.type === 'plugin'">
-						<template #key>{{ i18n.ts.description }}</template>
-						<template #value>{{ data.meta?.description }}</template>
-					</MkKeyValue>
-					<MkKeyValue v-if="data.type === 'plugin'">
-						<template #key>{{ i18n.ts.version }}</template>
-						<template #value>{{ data.meta?.version }}</template>
-					</MkKeyValue>
-					<MkKeyValue v-if="data.type === 'plugin'">
-						<template #key>{{ i18n.ts.permission }}</template>
-						<template #value>
-							<ul :class="$style.extInstallerKVList">
-								<li v-for="permission in data.meta?.permissions" :key="permission">{{ i18n.ts._permissions[permission] }}</li>
-							</ul>
-						</template>
-					</MkKeyValue>
-					<MkKeyValue v-if="data.type === 'theme' && data.meta?.base">
-						<template #key>{{ i18n.ts._externalResourceInstaller._meta.base }}</template>
-						<template #value>{{ i18n.ts[data.meta.base] }}</template>
-					</MkKeyValue>
-					<MkFolder>
-						<template #icon><i class="ti ti-code"></i></template>
-						<template #label>{{ i18n.ts._plugin.viewSource }}</template>
-
-						<MkCode :code="data.raw ?? ''"/>
-					</MkFolder>
-				</div>
-			</FormSection>
-			<FormSection>
-				<template #label>{{ i18n.ts._externalResourceInstaller._vendorInfo.title }}</template>
-				<div class="_gaps_s">
-					<MkKeyValue>
-						<template #key>{{ i18n.ts._externalResourceInstaller._vendorInfo.endpoint }}</template>
-						<template #value><MkUrl :url="url ?? ''" :showUrlPreview="false"></MkUrl></template>
-					</MkKeyValue>
-					<MkKeyValue>
-						<template #key>{{ i18n.ts._externalResourceInstaller._vendorInfo.hashVerify }}</template>
-						<template #value>
-							<!--この画面が出ている時点でハッシュの検証には成功している-->
-							<i class="ti ti-check" style="color: var(--accent)"></i>
-						</template>
-					</MkKeyValue>
-				</div>
-			</FormSection>
-			<div class="_buttonsCenter">
-				<MkButton primary @click="install()"><i class="ti ti-check"></i> {{ i18n.ts.install }}</MkButton>
-			</div>
-		</div>
+					</div>
+				</FormSection>
+			</template>
+		</MkExtensionInstaller>
 		<div v-else-if="uiPhase === 'error'" class="_gaps_m" :class="[$style.extInstallerRoot, $style.error]">
 			<div :class="$style.extInstallerIconWrapper">
 				<i class="ti ti-circle-x"></i>
@@ -96,14 +46,11 @@ SPDX-License-Identifier: AGPL-3.0-only
 <script lang="ts" setup>
 import { ref, computed, onActivated, onDeactivated, nextTick } from 'vue';
 import MkLoading from '@/components/global/MkLoading.vue';
+import MkExtensionInstaller, { type Extension } from '@/components/MkExtensionInstaller.vue';
 import MkButton from '@/components/MkButton.vue';
-import FormSection from '@/components/form/section.vue';
-import FormSplit from '@/components/form/split.vue';
-import MkCode from '@/components/MkCode.vue';
-import MkUrl from '@/components/global/MkUrl.vue';
-import MkInfo from '@/components/MkInfo.vue';
-import MkFolder from '@/components/MkFolder.vue';
 import MkKeyValue from '@/components/MkKeyValue.vue';
+import MkUrl from '@/components/global/MkUrl.vue';
+import FormSection from '@/components/form/section.vue';
 import * as os from '@/os.js';
 import { misskeyApi } from '@/scripts/misskey-api.js';
 import { AiScriptPluginMeta, parsePluginMeta, installPlugin } from '@/scripts/install-plugin.js';
@@ -124,24 +71,7 @@ const errorKV = ref<{
 const url = ref<string | null>(null);
 const hash = ref<string | null>(null);
 
-const data = ref<{
-	type: 'plugin' | 'theme';
-	raw: string;
-	meta?: {
-		// Plugin & Theme Common
-		name: string;
-		author: string;
-
-		// Plugin
-		description?: string;
-		version?: string;
-		permissions?: string[];
-		config?: Record<string, any>;
-
-		// Theme
-		base?: 'light' | 'dark';
-	};
-} | null>(null);
+const data = ref<Extension | null>(null);
 
 function goBack(): void {
 	history.back();
@@ -227,7 +157,7 @@ async function fetch() {
 				data.value = {
 					type: 'theme',
 					meta: {
-						description,
+						// description, // 使用されていない
 						...meta,
 					},
 					raw: res.data,
@@ -353,9 +283,4 @@ definePageMetadata(() => ({
 .extInstallerNormDesc {
 	text-align: center;
 }
-
-.extInstallerKVList {
-	margin-top: 0;
-	margin-bottom: 0;
-}
 </style>

From 3a11d5ede6d0d934d13252f8c27f411d9e28eb43 Mon Sep 17 00:00:00 2001
From: "github-actions[bot]" <github-actions[bot]@users.noreply.github.com>
Date: Mon, 7 Oct 2024 00:54:00 +0000
Subject: [PATCH 059/121] Bump version to 2024.10.0-beta.6

---
 package.json                     | 2 +-
 packages/misskey-js/package.json | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/package.json b/package.json
index 50c42645d3..8cb783d883 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
 	"name": "misskey",
-	"version": "2024.10.0-beta.5",
+	"version": "2024.10.0-beta.6",
 	"codename": "nasubi",
 	"repository": {
 		"type": "git",
diff --git a/packages/misskey-js/package.json b/packages/misskey-js/package.json
index 135ac60873..3be07d361e 100644
--- a/packages/misskey-js/package.json
+++ b/packages/misskey-js/package.json
@@ -1,7 +1,7 @@
 {
 	"type": "module",
 	"name": "misskey-js",
-	"version": "2024.10.0-beta.5",
+	"version": "2024.10.0-beta.6",
 	"description": "Misskey SDK for JavaScript",
 	"license": "MIT",
 	"main": "./built/index.js",

From 993d3fbe556d5151d32f3e111b41bb8be16295fb Mon Sep 17 00:00:00 2001
From: syuilo <4439005+syuilo@users.noreply.github.com>
Date: Tue, 8 Oct 2024 09:22:58 +0900
Subject: [PATCH 060/121] New Crowdin updates (#14722)

* New translations ja-jp.yml (Chinese Simplified)

* New translations ja-jp.yml (Chinese Simplified)
---
 locales/zh-CN.yml | 6 +++++-
 1 file changed, 5 insertions(+), 1 deletion(-)

diff --git a/locales/zh-CN.yml b/locales/zh-CN.yml
index c4bfe972fe..a8862d0a14 100644
--- a/locales/zh-CN.yml
+++ b/locales/zh-CN.yml
@@ -1290,6 +1290,10 @@ target: "对象"
 _abuseUserReport:
   forward: "转发"
   forwardDescription: "目标是匿名系统账户,将把举报转发给远程服务器。"
+  resolve: "解决"
+  accept: "确认"
+  reject: "拒绝"
+  resolveTutorial: "如果举报内容有理且已解决,选择「确认」将案件以肯定的态度标记为已解决。\n如果举报内容站不住脚,选择「拒绝」将案件以否定的态度标记为已解决。"
 _delivery:
   status: "投递状态"
   stop: "停止投递"
@@ -1626,7 +1630,7 @@ _achievements:
     _postedAt0min0sec:
       title: "报时"
       description: "在 0 点发布一篇帖子"
-      flavor: "报时信号最后一响,零点整"
+      flavor: "嘟 · 嘟 · 嘟 · 哔——"
     _selfQuote:
       title: "自我引用"
       description: "引用了自己的帖子"

From c14eba3e6d3087647a9cf5b16da1469e25288764 Mon Sep 17 00:00:00 2001
From: syuilo <4439005+syuilo@users.noreply.github.com>
Date: Tue, 8 Oct 2024 10:40:41 +0900
Subject: [PATCH 061/121] Update packages/frontend/src/store.ts

Co-authored-by: anatawa12 <anatawa12@icloud.com>
---
 packages/frontend/src/store.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/packages/frontend/src/store.ts b/packages/frontend/src/store.ts
index 7bb19aa2d7..cb52938980 100644
--- a/packages/frontend/src/store.ts
+++ b/packages/frontend/src/store.ts
@@ -226,7 +226,7 @@ export const defaultStore = markRaw(new Storage('base', {
 	},
 	animatedMfm: {
 		where: 'device',
-		default: window.matchMedia('(prefers-reduced-motion)').matches,
+		default: !window.matchMedia('(prefers-reduced-motion)').matches,
 	},
 	advancedMfm: {
 		where: 'device',

From 9858e12078f4f4d223e159796e1205a39c7c03b5 Mon Sep 17 00:00:00 2001
From: syuilo <4439005+syuilo@users.noreply.github.com>
Date: Tue, 8 Oct 2024 18:50:09 +0900
Subject: [PATCH 062/121] New Crowdin updates (#14723)

* New translations ja-jp.yml (English)

* New translations ja-jp.yml (English)

* New translations ja-jp.yml (Portuguese)

* New translations ja-jp.yml (Chinese Simplified)
---
 locales/en-US.yml | 52 +++++++++++++++++++++++++++++------------------
 locales/pt-PT.yml |  6 +++---
 locales/zh-CN.yml |  8 ++++----
 3 files changed, 39 insertions(+), 27 deletions(-)

diff --git a/locales/en-US.yml b/locales/en-US.yml
index 7b275c990c..126f769644 100644
--- a/locales/en-US.yml
+++ b/locales/en-US.yml
@@ -112,7 +112,7 @@ enterEmoji: "Enter an emoji"
 renote: "Renote"
 unrenote: "Remove renote"
 renoted: "Renoted."
-renotedToX: "Renote to {name}."
+renotedToX: "Renoted to {name}."
 cantRenote: "This post can't be renoted."
 cantReRenote: "A renote can't be renoted."
 quote: "Quote"
@@ -454,6 +454,7 @@ totpDescription: "Use an authenticator app to enter one-time passwords"
 moderator: "Moderator"
 moderation: "Moderation"
 moderationNote: "Moderation note"
+moderationNoteDescription: "You can fill in notes that will be shared only among moderators."
 addModerationNote: "Add moderation note"
 moderationLogs: "Moderation logs"
 nUsersMentioned: "Mentioned by {n} users"
@@ -921,6 +922,7 @@ followersVisibility: "Visibility of followers"
 continueThread: "View thread continuation"
 deleteAccountConfirm: "This will irreversibly delete your account. Proceed?"
 incorrectPassword: "Incorrect password."
+incorrectTotp: "The one-time password is incorrect or has expired."
 voteConfirm: "Confirm your vote for \"{choice}\"?"
 hide: "Hide"
 useDrawerReactionPickerForMobile: "Display reaction picker as drawer on mobile"
@@ -1284,6 +1286,14 @@ unknownWebAuthnKey: "Unknown Passkey"
 passkeyVerificationFailed: "Passkey verification has failed."
 passkeyVerificationSucceededButPasswordlessLoginDisabled: "Passkey verification has succeeded but password-less login is disabled."
 messageToFollower: "Message to followers"
+target: "Target"
+_abuseUserReport:
+  forward: "Forward"
+  forwardDescription: "Forward the report to a remote server as an anonymous system account."
+  resolve: "Resolve"
+  accept: "Accept"
+  reject: "Reject"
+  resolveTutorial: "If the report is legitimate in content, select \"Accept\" to mark the case as resolved in the affirmative.\nIf the content of the report is not legitimate, select \"Reject\" to mark the case as resolved in the negative."
 _delivery:
   status: "Delivery status"
   stop: "Suspended"
@@ -1737,7 +1747,7 @@ _role:
     canManageAvatarDecorations: "Manage avatar decorations"
     driveCapacity: "Drive capacity"
     alwaysMarkNsfw: "Always mark files as NSFW"
-    canUpdateBioMedia: "Allow to edit an icon or a banner image"
+    canUpdateBioMedia: "Can edit an icon or a banner image"
     pinMax: "Maximum number of pinned notes"
     antennaMax: "Maximum number of antennas"
     wordMuteMax: "Maximum number of characters allowed in word mutes"
@@ -2473,22 +2483,22 @@ _webhookSettings:
     reaction: "When receiving a reaction"
     mention: "When being mentioned"
   _systemEvents:
-    abuseReport: "When received a new abuse report"
-    abuseReportResolved: "When resolved abuse report"
+    abuseReport: "When received a new report"
+    abuseReportResolved: "When resolved report"
     userCreated: "When user is created"
   deleteConfirm: "Are you sure you want to delete the Webhook?"
   testRemarks: "Click the button to the right of the switch to send a test Webhook with dummy data."
 _abuseReport:
   _notificationRecipient:
-    createRecipient: "Add a recipient for abuse reports"
-    modifyRecipient: "Edit a recipient for abuse reports"
+    createRecipient: "Add a recipient for reports"
+    modifyRecipient: "Edit a recipient for reports"
     recipientType: "Notification type"
     _recipientType:
       mail: "Email"
       webhook: "Webhook"
       _captions:
-        mail: "Send the email to moderators' email addresses when you receive abuse."
-        webhook: "Send a notification to SystemWebhook when you receive or resolve abuse."
+        mail: "Send the email to moderators' email addresses when you receive reports."
+        webhook: "Send a notification to System Webhook when you receive or resolve reports."
     keywords: "Keywords"
     notifiedUser: "Users to notify"
     notifiedWebhook: "Webhook to use"
@@ -2521,6 +2531,8 @@ _moderationLogTypes:
   markSensitiveDriveFile: "File marked as sensitive"
   unmarkSensitiveDriveFile: "File unmarked as sensitive"
   resolveAbuseReport: "Report resolved"
+  forwardAbuseReport: "Report forwarded"
+  updateAbuseReportNote: "Moderation note of a report updated"
   createInvitation: "Invite generated"
   createAd: "Ad created"
   deleteAd: "Ad deleted"
@@ -2528,18 +2540,18 @@ _moderationLogTypes:
   createAvatarDecoration: "Avatar decoration created"
   updateAvatarDecoration: "Avatar decoration updated"
   deleteAvatarDecoration: "Avatar decoration deleted"
-  unsetUserAvatar: "Unset this user's avatar"
-  unsetUserBanner: "Unset this user's banner"
-  createSystemWebhook: "Create SystemWebhook"
-  updateSystemWebhook: "Update SystemWebhook"
-  deleteSystemWebhook: "Delete SystemWebhook"
-  createAbuseReportNotificationRecipient: "Create a recipient for abuse reports"
-  updateAbuseReportNotificationRecipient: "Update recipients for abuse reports"
-  deleteAbuseReportNotificationRecipient: "Delete a recipient for abuse reports"
-  deleteAccount: "Delete the account"
-  deletePage: "Delete the page"
-  deleteFlash: "Delete Play"
-  deleteGalleryPost: "Delete the gallery post"
+  unsetUserAvatar: "User avatar unset"
+  unsetUserBanner: "User banner unset"
+  createSystemWebhook: "System Webhook created"
+  updateSystemWebhook: "System Webhook updated"
+  deleteSystemWebhook: "System Webhook deleted"
+  createAbuseReportNotificationRecipient: "Recipient for reports created"
+  updateAbuseReportNotificationRecipient: "Recipient for reports updated"
+  deleteAbuseReportNotificationRecipient: "Recipient for reports deleted"
+  deleteAccount: "Account deleted"
+  deletePage: "Page deleted"
+  deleteFlash: "Play deleted"
+  deleteGalleryPost: "Gallery post deleted"
 _fileViewer:
   title: "File details"
   type: "File type"
diff --git a/locales/pt-PT.yml b/locales/pt-PT.yml
index dac4abbe64..98d42eb44a 100644
--- a/locales/pt-PT.yml
+++ b/locales/pt-PT.yml
@@ -1,5 +1,5 @@
 ---
-_lang_: "日本語"
+_lang_: "Português"
 headlineMisskey: "Uma rede ligada por notas"
 introMisskey: "Bem-vindo! O Misskey é um serviço de microblog descentralizado de código aberto.\nCrie \"notas\" para compartilhar o que está acontecendo agora ou para se expressar com todos à sua volta 📡\nVocê também pode adicionar rapidamente reações às notas de outras pessoas usando a função \"Reações\" 👍\nVamos explorar um novo mundo 🚀"
 poweredByMisskeyDescription: "{name} é uma instância da plataforma de código aberto <b>Misskey</b>."
@@ -1058,7 +1058,7 @@ resetPasswordConfirm: "Deseja realmente mudar a sua senha?"
 sensitiveWords: "Palavras sensíveis"
 sensitiveWordsDescription: "A visibilidade de todas as notas contendo as palavras configuradas será colocadas como \"Início\" automaticamente. Você pode listar várias delas separando-as por linha."
 sensitiveWordsDescription2: "Utilizar espaços irá criar expressões aditivas (AND) e cercar palavras-chave com barras irá transformá-las em expressões regulares (RegEx)"
-prohibitedWords: "Palavras proibídas"
+prohibitedWords: "Palavras proibidas"
 prohibitedWordsDescription: "Habilita um erro ao tentar publicar uma nota contendo as palavras escolhidas. Várias palavras podem ser escolhidas, separando-as por linha."
 prohibitedWordsDescription2: "Utilizar espaços irá criar expressões aditivas (AND) e cercar palavras-chave com barras irá transformá-las em expressões regulares (RegEx)"
 hiddenTags: "Hashtags escondidas"
@@ -1416,7 +1416,7 @@ _achievements:
   _types:
     _notes1:
       title: "Configurando o meu misskey"
-      description: "Post uma nota pela primeira vez"
+      description: "Poste uma nota pela primeira vez"
       flavor: "Divirta-se com o Misskey!"
     _notes10:
       title: "Algumas notas"
diff --git a/locales/zh-CN.yml b/locales/zh-CN.yml
index a8862d0a14..09feca5b4e 100644
--- a/locales/zh-CN.yml
+++ b/locales/zh-CN.yml
@@ -1199,10 +1199,10 @@ followingOrFollower: "关注中或关注者"
 fileAttachedOnly: "仅限媒体"
 showRepliesToOthersInTimeline: "在时间线中包含给别人的回复"
 hideRepliesToOthersInTimeline: "在时间线中隐藏给别人的回复"
-showRepliesToOthersInTimelineAll: "在时间线中包含现在关注的所有人的回复"
-hideRepliesToOthersInTimelineAll: "在时间线中隐藏现在关注的所有人的回复"
-confirmShowRepliesAll: "此操作不可撤销。确认要在时间线中包含现在关注的所有人的回复吗?"
-confirmHideRepliesAll: "此操作不可撤销。确认要在时间线中隐藏现在关注的所有人的回复吗?"
+showRepliesToOthersInTimelineAll: "在时间线中显示所有现在关注的人的回复"
+hideRepliesToOthersInTimelineAll: "在时间线中隐藏所有现在关注的人的回复"
+confirmShowRepliesAll: "此操作不可撤销。确认要在时间线中显示所有现在关注的人的回复吗?"
+confirmHideRepliesAll: "此操作不可撤销。确认要在时间线中隐藏所有现在关注的人的回复吗?"
 externalServices: "外部服务"
 sourceCode: "源代码"
 sourceCodeIsNotYetProvided: "还未提供源代码。要解决此问题请联系管理员。"

From d0213962bf6c893f2883130a2051b26975a321a7 Mon Sep 17 00:00:00 2001
From: syuilo <4439005+syuilo@users.noreply.github.com>
Date: Tue, 8 Oct 2024 18:59:10 +0900
Subject: [PATCH 063/121] Update
 packages/backend/src/core/entities/FlashEntityService.ts

Co-authored-by: zyoshoka <107108195+zyoshoka@users.noreply.github.com>
---
 packages/backend/src/core/entities/FlashEntityService.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/packages/backend/src/core/entities/FlashEntityService.ts b/packages/backend/src/core/entities/FlashEntityService.ts
index 0cdcf3310a..7b0150f5b6 100644
--- a/packages/backend/src/core/entities/FlashEntityService.ts
+++ b/packages/backend/src/core/entities/FlashEntityService.ts
@@ -40,7 +40,7 @@ export class FlashEntityService {
 		// { schema: 'UserDetailed' } すると無限ループするので注意
 		const user = hint?.packedUser ?? await this.userEntityService.pack(flash.user ?? flash.userId, me);
 
-		let isLiked = false;
+		let isLiked = undefined;
 		if (meId) {
 			isLiked = hint?.likedFlashIds
 				? hint.likedFlashIds.includes(flash.id)

From dd39c5e059dea5326d36a063ab29d55d56366033 Mon Sep 17 00:00:00 2001
From: syuilo <4439005+syuilo@users.noreply.github.com>
Date: Wed, 9 Oct 2024 09:47:28 +0900
Subject: [PATCH 064/121] Update
 packages/frontend/src/components/MkAbuseReport.vue
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Co-authored-by: かっこかり <67428053+kakkokari-gtyih@users.noreply.github.com>
---
 packages/frontend/src/components/MkAbuseReport.vue | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/packages/frontend/src/components/MkAbuseReport.vue b/packages/frontend/src/components/MkAbuseReport.vue
index 2f0e09fc4b..0278cb30f0 100644
--- a/packages/frontend/src/components/MkAbuseReport.vue
+++ b/packages/frontend/src/components/MkAbuseReport.vue
@@ -21,7 +21,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 				<MkButton @click="resolve('reject')"><i class="ti ti-x" style="color: var(--error)"></i> {{ i18n.ts._abuseUserReport.resolve }} ({{ i18n.ts._abuseUserReport.reject }})</MkButton>
 				<MkButton @click="resolve(null)"><i class="ti ti-slash"></i> {{ i18n.ts._abuseUserReport.resolve }} ({{ i18n.ts.other }})</MkButton>
 			</template>
-			<template v-if="report.targetUser.host == null">
+			<template v-if="report.targetUser.host != null">
 				<MkButton :disabled="report.forwarded" primary @click="forward"><i class="ti ti-corner-up-right"></i> {{ i18n.ts._abuseUserReport.forward }}</MkButton>
 				<div v-tooltip:dialog="i18n.ts._abuseUserReport.forwardDescription" class="_button _help"><i class="ti ti-help-circle"></i></div>
 			</template>

From 0da6f14b3b47cbe90eaeb97035da82ac5926f6c4 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Wed, 9 Oct 2024 10:25:01 +0900
Subject: [PATCH 065/121] build(deps): bump actions/cache from 4.0.2 to 4.1.0
 (#14718)

Bumps [actions/cache](https://github.com/actions/cache) from 4.0.2 to 4.1.0.
- [Release notes](https://github.com/actions/cache/releases)
- [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md)
- [Commits](https://github.com/actions/cache/compare/v4.0.2...v4.1.0)

---
updated-dependencies:
- dependency-name: actions/cache
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
 .github/workflows/lint.yml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
index 07d9af12f7..90eb268dda 100644
--- a/.github/workflows/lint.yml
+++ b/.github/workflows/lint.yml
@@ -75,7 +75,7 @@ jobs:
     - run: corepack enable
     - run: pnpm i --frozen-lockfile
     - name: Restore eslint cache
-      uses: actions/cache@v4.0.2
+      uses: actions/cache@v4.1.0
       with:
         path: ${{ env.eslint-cache-path }}
         key: eslint-${{ env.eslint-cache-version }}-${{ matrix.workspace }}-${{ hashFiles('**/pnpm-lock.yaml') }}-${{ github.ref_name }}-${{ github.sha }}

From c13545f965fc4055d1e79c739125cb5644263620 Mon Sep 17 00:00:00 2001
From: syuilo <4439005+syuilo@users.noreply.github.com>
Date: Wed, 9 Oct 2024 11:58:51 +0900
Subject: [PATCH 066/121] :art:

---
 .../frontend/src/components/MkNoteHeader.vue  | 20 ++++++++-----------
 1 file changed, 8 insertions(+), 12 deletions(-)

diff --git a/packages/frontend/src/components/MkNoteHeader.vue b/packages/frontend/src/components/MkNoteHeader.vue
index cf689d1fee..a75b9ddd10 100644
--- a/packages/frontend/src/components/MkNoteHeader.vue
+++ b/packages/frontend/src/components/MkNoteHeader.vue
@@ -5,18 +5,14 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <template>
 <header :class="$style.root">
-	<component :is="defaultStore.state.enableCondensedLine ? 'MkCondensedLine' : 'div'" :minScale="0.7" style="min-width: 0;">
-		<div style="display: flex; white-space: nowrap; align-items: baseline;">
-			<div v-if="mock" :class="$style.name">
-				<MkUserName :user="note.user"/>
-			</div>
-			<MkA v-else v-user-preview="note.user.id" :class="$style.name" :to="userPage(note.user)">
-				<MkUserName :user="note.user"/>
-			</MkA>
-			<div v-if="note.user.isBot" :class="$style.isBot">bot</div>
-			<div :class="$style.username"><MkAcct :user="note.user"/></div>
-		</div>
-	</component>
+	<div v-if="mock" :class="$style.name">
+		<MkUserName :user="note.user"/>
+	</div>
+	<MkA v-else v-user-preview="note.user.id" :class="$style.name" :to="userPage(note.user)">
+		<MkUserName :user="note.user"/>
+	</MkA>
+	<div v-if="note.user.isBot" :class="$style.isBot">bot</div>
+	<div :class="$style.username"><MkAcct :user="note.user"/></div>
 	<div v-if="note.user.badgeRoles" :class="$style.badgeRoles">
 		<img v-for="(role, i) in note.user.badgeRoles" :key="i" v-tooltip="role.name" :class="$style.badgeRole" :src="role.iconUrl!"/>
 	</div>

From a304185eb846977211560bbff2060bc1f7903ce0 Mon Sep 17 00:00:00 2001
From: syuilo <4439005+syuilo@users.noreply.github.com>
Date: Wed, 9 Oct 2024 14:07:05 +0900
Subject: [PATCH 067/121] Update CHANGELOG.md

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

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 405ee7c10a..b32f63a3c2 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,7 +1,7 @@
 ## 2024.10.0
 
 ### Note
-- サーバー初期設定時に使用する初期パスワードを設定できるようになりました。今後Misskeyサーバーを新たに設置する際には、初回の起動前にコンフィグファイルの`setupPassword`をコメントアウトし、初期パスワードを設定することをおすすめします。(すでに初期設定を完了しているサーバーについては、この変更に伴い対応する必要はありません)  
+- セキュリティ向上のため、サーバー初期設定時に使用する初期パスワードを設定できるようになりました。今後Misskeyサーバーを新たに設置する際には、初回の起動前にコンフィグファイルの`setupPassword`をコメントアウトし、初期パスワードを設定することをおすすめします。(すでに初期設定を完了しているサーバーについては、この変更に伴い対応する必要はありません)  
   - ホスティングサービスを運営している場合は、コンフィグファイルを構築する際に`setupPassword`をランダムな値に設定し、ユーザーに通知するようにシステムを更新することをおすすめします。
   - なお、初期パスワードが設定されていない場合でも初期設定を行うことが可能です(UI上で初期パスワードの入力欄を空欄にすると続行できます)。
 - ユーザーデータを読み込む際の型が一部変更されました。

From 6de7c275221996011b03699a6f618909100cd44e Mon Sep 17 00:00:00 2001
From: "github-actions[bot]" <github-actions[bot]@users.noreply.github.com>
Date: Wed, 9 Oct 2024 05:17:26 +0000
Subject: [PATCH 068/121] Release: 2024.10.0

---
 package.json                     | 2 +-
 packages/misskey-js/package.json | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/package.json b/package.json
index 8cb783d883..3afd84253a 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
 	"name": "misskey",
-	"version": "2024.10.0-beta.6",
+	"version": "2024.10.0",
 	"codename": "nasubi",
 	"repository": {
 		"type": "git",
diff --git a/packages/misskey-js/package.json b/packages/misskey-js/package.json
index 3be07d361e..a7e04d6dac 100644
--- a/packages/misskey-js/package.json
+++ b/packages/misskey-js/package.json
@@ -1,7 +1,7 @@
 {
 	"type": "module",
 	"name": "misskey-js",
-	"version": "2024.10.0-beta.6",
+	"version": "2024.10.0",
 	"description": "Misskey SDK for JavaScript",
 	"license": "MIT",
 	"main": "./built/index.js",

From 0ad31bd5d42d7caf4bafe1e5b8c1f1f55a0cb55d Mon Sep 17 00:00:00 2001
From: "github-actions[bot]" <github-actions[bot]@users.noreply.github.com>
Date: Wed, 9 Oct 2024 05:17:31 +0000
Subject: [PATCH 069/121] [skip ci] Update CHANGELOG.md (prepend template)

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

diff --git a/CHANGELOG.md b/CHANGELOG.md
index b32f63a3c2..e8909c15da 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,15 @@
+## Unreleased
+
+### General
+-
+
+### Client
+-
+
+### Server
+-
+
+
 ## 2024.10.0
 
 ### Note

From 4a356f1ba742ae3965d01ad17179d3af4846377a Mon Sep 17 00:00:00 2001
From: syuilo <4439005+syuilo@users.noreply.github.com>
Date: Wed, 9 Oct 2024 18:08:14 +0900
Subject: [PATCH 070/121] refactor(frontend): prefix css variables (#14725)

* wip

* Update index.d.ts

* remove unnecessary codes
---
 CONTRIBUTING.md                               |  6 +-
 idea/MkDisableSection.vue                     |  2 +-
 locales/index.d.ts                            |  4 -
 locales/ja-JP.yml                             |  1 -
 packages/backend/src/server/web/boot.js       |  2 +-
 packages/backend/src/server/web/style.css     |  8 +-
 .../backend/src/server/web/style.embed.css    | 10 +--
 .../src/components/EmLoading.vue              |  2 +-
 .../src/components/EmMediaBanner.vue          |  8 +-
 .../src/components/EmMediaImage.vue           | 18 ++--
 .../src/components/EmMediaVideo.vue           |  4 +-
 .../src/components/EmMention.vue              |  4 +-
 .../frontend-embed/src/components/EmMfm.ts    | 10 +--
 .../frontend-embed/src/components/EmNote.vue  | 24 ++---
 .../src/components/EmNoteDetailed.vue         | 22 ++---
 .../src/components/EmNoteHeader.vue           |  2 +-
 .../src/components/EmNoteSub.vue              |  4 +-
 .../frontend-embed/src/components/EmNotes.vue |  4 +-
 .../frontend-embed/src/components/EmPoll.vue  | 14 +--
 .../components/EmReactionsViewer.reaction.vue | 10 +--
 .../src/components/EmSubNoteContent.vue       | 12 +--
 .../frontend-embed/src/components/EmTime.vue  |  4 +-
 .../src/components/EmTimelineContainer.vue    |  4 +-
 packages/frontend-embed/src/pages/clip.vue    |  4 +-
 packages/frontend-embed/src/pages/note.vue    |  2 +-
 packages/frontend-embed/src/pages/tag.vue     |  4 +-
 packages/frontend-embed/src/style.scss        | 50 +++++------
 packages/frontend-embed/src/theme.ts          |  2 +-
 packages/frontend-embed/src/ui.vue            |  4 +-
 packages/frontend-shared/themes/_dark.json5   |  3 +-
 packages/frontend-shared/themes/_light.json5  |  3 +-
 packages/frontend-shared/themes/d-astro.json5 |  3 +-
 packages/frontend-shared/themes/d-u0.json5    |  3 +-
 packages/frontend-shared/themes/l-u0.json5    |  3 +-
 packages/frontend-shared/themes/l-vivid.json5 |  3 +-
 packages/frontend/src/_dev_boot_.ts           |  2 +-
 packages/frontend/src/boot/common.ts          |  6 --
 .../frontend/src/components/MkAbuseReport.vue | 10 +--
 .../src/components/MkAccountMoved.vue         |  4 +-
 .../frontend/src/components/MkAnalogClock.vue |  6 +-
 .../src/components/MkAnnouncementDialog.vue   |  8 +-
 .../src/components/MkAntennaEditor.vue        |  2 +-
 packages/frontend/src/components/MkAsUi.vue   |  4 +-
 .../src/components/MkAutocomplete.vue         |  6 +-
 packages/frontend/src/components/MkButton.vue | 22 ++---
 .../src/components/MkChannelFollowButton.vue  | 16 ++--
 .../src/components/MkChannelPreview.vue       | 12 +--
 .../frontend/src/components/MkChartLegend.vue |  4 +-
 .../frontend/src/components/MkClipPreview.vue |  6 +-
 .../frontend/src/components/MkCode.core.vue   |  2 +-
 packages/frontend/src/components/MkCode.vue   |  6 +-
 .../frontend/src/components/MkCodeEditor.vue  | 14 +--
 .../frontend/src/components/MkCodeInline.vue  |  2 +-
 .../frontend/src/components/MkColorInput.vue  | 14 +--
 .../frontend/src/components/MkContainer.vue   | 12 +--
 .../src/components/MkCropperDialog.vue        |  2 +-
 .../MkCustomEmojiDetailedDialog.vue           |  6 +-
 .../src/components/MkDateSeparatedList.vue    |  4 +-
 packages/frontend/src/components/MkDialog.vue |  8 +-
 .../frontend/src/components/MkDivider.vue     |  2 +-
 .../frontend/src/components/MkDonation.vue    |  2 +-
 .../frontend/src/components/MkDrive.file.vue  |  8 +-
 .../src/components/MkDrive.folder.vue         | 12 +--
 packages/frontend/src/components/MkDrive.vue  |  4 +-
 .../src/components/MkDriveFileThumbnail.vue   |  4 +-
 .../src/components/MkEmbedCodeGenDialog.vue   |  8 +-
 .../src/components/MkEmojiPicker.section.vue  |  4 +-
 .../frontend/src/components/MkEmojiPicker.vue | 24 ++---
 .../src/components/MkExtensionInstaller.vue   |  6 +-
 .../src/components/MkFileListForAdmin.vue     |  2 +-
 .../src/components/MkFlashPreview.vue         |  4 +-
 .../src/components/MkFoldableSection.vue      |  4 +-
 packages/frontend/src/components/MkFolder.vue | 24 ++---
 .../src/components/MkFollowButton.vue         | 16 ++--
 .../src/components/MkFormDialog.file.vue      |  2 +-
 .../frontend/src/components/MkFormFooter.vue  |  2 +-
 .../frontend/src/components/MkFukidashi.vue   |  4 +-
 .../src/components/MkGalleryPostPreview.vue   |  2 +-
 packages/frontend/src/components/MkGoogle.vue |  4 +-
 packages/frontend/src/components/MkInfo.vue   |  8 +-
 packages/frontend/src/components/MkInput.vue  | 14 +--
 .../src/components/MkInstanceCardMini.vue     |  6 +-
 .../src/components/MkInstanceStats.vue        |  4 +-
 .../frontend/src/components/MkInviteCode.vue  |  4 +-
 .../frontend/src/components/MkLaunchPad.vue   |  6 +-
 .../frontend/src/components/MkMediaAudio.vue  |  8 +-
 .../frontend/src/components/MkMediaImage.vue  | 20 ++---
 .../frontend/src/components/MkMediaList.vue   |  8 +-
 .../frontend/src/components/MkMediaRange.vue  |  4 +-
 .../frontend/src/components/MkMediaVideo.vue  | 18 ++--
 .../frontend/src/components/MkMention.vue     |  8 +-
 packages/frontend/src/components/MkMenu.vue   | 32 +++----
 .../frontend/src/components/MkMiniChart.vue   |  2 +-
 .../frontend/src/components/MkModalWindow.vue | 20 ++---
 packages/frontend/src/components/MkNote.vue   | 28 +++---
 .../src/components/MkNoteDetailed.vue         | 34 +++----
 .../frontend/src/components/MkNoteHeader.vue  |  2 +-
 .../frontend/src/components/MkNoteSub.vue     |  4 +-
 packages/frontend/src/components/MkNotes.vue  |  6 +-
 .../src/components/MkNotification.vue         | 10 +--
 .../src/components/MkNotifications.vue        |  2 +-
 .../frontend/src/components/MkNumberDiff.vue  |  4 +-
 .../src/components/MkObjectView.value.vue     |  8 +-
 packages/frontend/src/components/MkOmit.vue   |  6 +-
 .../frontend/src/components/MkPagePreview.vue |  6 +-
 .../frontend/src/components/MkPageWindow.vue  |  2 +-
 packages/frontend/src/components/MkPoll.vue   | 14 +--
 .../frontend/src/components/MkPostForm.vue    | 30 +++----
 .../src/components/MkPostFormAttaches.vue     |  2 +-
 packages/frontend/src/components/MkRadio.vue  | 20 ++---
 packages/frontend/src/components/MkRadios.vue |  2 +-
 packages/frontend/src/components/MkRange.vue  | 14 +--
 .../src/components/MkReactionEffect.vue       |  2 +-
 .../components/MkReactionsViewer.details.vue  |  2 +-
 .../components/MkReactionsViewer.reaction.vue | 10 +--
 .../src/components/MkRemoteCaution.vue        |  6 +-
 .../src/components/MkRetentionLineChart.vue   |  2 +-
 .../src/components/MkRippleEffect.vue         |  4 +-
 .../frontend/src/components/MkRolePreview.vue |  8 +-
 packages/frontend/src/components/MkSelect.vue | 14 +--
 .../src/components/MkSignin.input.vue         | 10 +--
 .../src/components/MkSignin.passkey.vue       |  4 +-
 .../src/components/MkSignin.password.vue      |  6 +-
 .../frontend/src/components/MkSignin.totp.vue |  4 +-
 packages/frontend/src/components/MkSignin.vue |  2 +-
 .../src/components/MkSigninDialog.vue         |  4 +-
 .../src/components/MkSignupDialog.form.vue    | 44 +++++-----
 .../src/components/MkSignupDialog.rules.vue   | 14 +--
 .../components/MkSourceCodeAvailablePopup.vue |  2 +-
 .../src/components/MkSubNoteContent.vue       | 12 +--
 .../frontend/src/components/MkSuperMenu.vue   | 16 ++--
 .../src/components/MkSwitch.button.vue        | 12 +--
 packages/frontend/src/components/MkSwitch.vue |  8 +-
 .../src/components/MkSystemWebhookEditor.vue  |  6 +-
 packages/frontend/src/components/MkTab.vue    |  8 +-
 .../frontend/src/components/MkTagCloud.vue    |  2 +-
 .../frontend/src/components/MkTextarea.vue    | 12 +--
 .../src/components/MkTokenGenerateWindow.vue  |  6 +-
 .../frontend/src/components/MkTooltip.vue     |  2 +-
 .../src/components/MkTutorialDialog.Note.vue  |  8 +-
 .../components/MkTutorialDialog.PostNote.vue  | 10 +--
 .../components/MkTutorialDialog.Sensitive.vue | 12 +--
 .../components/MkTutorialDialog.Timeline.vue  | 10 +--
 .../src/components/MkTutorialDialog.vue       |  8 +-
 .../frontend/src/components/MkUpdated.vue     |  2 +-
 .../frontend/src/components/MkUrlPreview.vue  |  6 +-
 .../MkUserAnnouncementEditDialog.vue          |  8 +-
 .../src/components/MkUserCardMini.vue         |  4 +-
 .../frontend/src/components/MkUserInfo.vue    | 12 +--
 .../src/components/MkUserOnlineIndicator.vue  |  2 +-
 .../frontend/src/components/MkUserPopup.vue   | 10 +--
 .../src/components/MkUserSelectDialog.vue     |  4 +-
 .../src/components/MkUserSetupDialog.User.vue |  6 +-
 .../src/components/MkUserSetupDialog.vue      | 10 +--
 .../src/components/MkVisibilityPicker.vue     |  2 +-
 .../MkVisitorDashboard.ActiveUsersChart.vue   |  2 +-
 .../src/components/MkVisitorDashboard.vue     |  8 +-
 .../src/components/MkWaitingDialog.vue        |  4 +-
 packages/frontend/src/components/MkWindow.vue | 10 +--
 .../frontend/src/components/form/link.vue     | 10 +--
 .../frontend/src/components/form/section.vue  |  6 +-
 .../frontend/src/components/form/slot.vue     |  2 +-
 .../frontend/src/components/global/MkAd.vue   |  4 +-
 .../src/components/global/MkLoading.vue       |  2 +-
 .../frontend/src/components/global/MkMfm.ts   | 10 +--
 .../components/global/MkPageHeader.tabs.vue   |  2 +-
 .../src/components/global/MkPageHeader.vue    |  6 +-
 .../frontend/src/components/global/MkTime.vue |  4 +-
 .../src/components/page/page.dynamic.vue      |  2 +-
 .../src/components/page/page.image.vue        |  2 +-
 .../src/components/page/page.note.vue         |  2 +-
 .../frontend/src/directives/adaptive-bg.ts    |  2 +-
 .../src/directives/adaptive-border.ts         |  2 +-
 packages/frontend/src/directives/panel.ts     |  6 +-
 packages/frontend/src/pages/about-misskey.vue | 10 +--
 .../frontend/src/pages/about.overview.vue     |  6 +-
 packages/frontend/src/pages/admin-user.vue    | 20 ++---
 .../src/pages/admin/RolesEditorFormula.vue    |  4 +-
 .../frontend/src/pages/admin/_header_.vue     |  6 +-
 .../notification-recipient.editor.vue         |  4 +-
 .../notification-recipient.item.vue           |  4 +-
 .../src/pages/admin/announcements.vue         | 12 +--
 packages/frontend/src/pages/admin/index.vue   |  2 +-
 .../src/pages/admin/modlog.ModLog.vue         |  6 +-
 .../src/pages/admin/overview.ap-requests.vue  |  2 +-
 .../src/pages/admin/overview.federation.vue   |  4 +-
 .../frontend/src/pages/admin/overview.pie.vue |  2 +-
 .../src/pages/admin/overview.queue.vue        |  2 +-
 .../src/pages/admin/overview.stats.vue        |  4 +-
 .../frontend/src/pages/admin/queue.chart.vue  |  2 +-
 packages/frontend/src/pages/admin/relays.vue  |  4 +-
 .../frontend/src/pages/admin/roles.role.vue   |  2 +-
 .../frontend/src/pages/admin/server-rules.vue | 10 +--
 .../frontend/src/pages/admin/settings.vue     |  2 +-
 .../src/pages/admin/system-webhook.item.vue   |  6 +-
 packages/frontend/src/pages/announcement.vue  |  6 +-
 packages/frontend/src/pages/announcements.vue |  6 +-
 .../frontend/src/pages/antenna-timeline.vue   |  2 +-
 .../frontend/src/pages/channel-editor.vue     |  2 +-
 packages/frontend/src/pages/channel.vue       |  8 +-
 packages/frontend/src/pages/clip.vue          |  2 +-
 .../src/pages/custom-emojis-manager.vue       |  8 +-
 .../frontend/src/pages/drive.file.info.vue    | 16 ++--
 .../src/pages/drop-and-fusion.game.vue        |  2 +-
 .../frontend/src/pages/emoji-edit-dialog.vue  |  4 +-
 packages/frontend/src/pages/emojis.emoji.vue  |  4 +-
 packages/frontend/src/pages/favorites.vue     |  2 +-
 .../frontend/src/pages/flash/flash-edit.vue   |  4 +-
 packages/frontend/src/pages/flash/flash.vue   |  4 +-
 packages/frontend/src/pages/gallery/edit.vue  |  4 +-
 packages/frontend/src/pages/gallery/post.vue  | 14 +--
 packages/frontend/src/pages/games.vue         |  2 +-
 .../frontend/src/pages/install-extensions.vue |  8 +-
 .../frontend/src/pages/my-antennas/index.vue  |  4 +-
 .../frontend/src/pages/my-lists/index.vue     |  4 +-
 packages/frontend/src/pages/my-lists/list.vue |  4 +-
 packages/frontend/src/pages/note.vue          |  2 +-
 .../page-editor/els/page-editor.el.text.vue   |  2 +-
 .../page-editor/page-editor.container.vue     |  6 +-
 packages/frontend/src/pages/page.vue          | 18 ++--
 .../frontend/src/pages/reversi/game.board.vue | 16 ++--
 .../src/pages/reversi/game.setting.vue        | 12 +--
 packages/frontend/src/pages/reversi/index.vue | 18 ++--
 packages/frontend/src/pages/scratchpad.vue    |  4 +-
 packages/frontend/src/pages/search.note.vue   |  4 +-
 packages/frontend/src/pages/settings/2fa.vue  |  2 +-
 .../settings/avatar-decoration.decoration.vue |  6 +-
 .../settings/avatar-decoration.dialog.vue     |  2 +-
 .../src/pages/settings/drive-cleaner.vue      |  2 +-
 .../frontend/src/pages/settings/email.vue     |  2 +-
 .../src/pages/settings/emoji-picker.vue       |  4 +-
 .../src/pages/settings/mute-block.vue         |  2 +-
 .../frontend/src/pages/settings/navbar.vue    |  2 +-
 .../frontend/src/pages/settings/profile.vue   |  2 +-
 .../frontend/src/pages/settings/security.vue  |  6 +-
 .../src/pages/settings/sounds.sound.vue       |  4 +-
 .../frontend/src/pages/settings/theme.vue     | 12 +--
 .../src/pages/settings/webhook.edit.vue       |  2 +-
 .../frontend/src/pages/settings/webhook.vue   |  4 +-
 .../frontend/src/pages/signup-complete.vue    |  4 +-
 packages/frontend/src/pages/tag.vue           |  4 +-
 packages/frontend/src/pages/theme-editor.vue  |  2 +-
 packages/frontend/src/pages/timeline.vue      |  2 +-
 .../frontend/src/pages/user-list-timeline.vue |  2 +-
 packages/frontend/src/pages/user/clips.vue    |  2 +-
 packages/frontend/src/pages/user/home.vue     | 28 +++---
 .../src/pages/user/index.timeline.vue         |  4 +-
 packages/frontend/src/pages/user/lists.vue    |  4 +-
 packages/frontend/src/pages/user/raw.vue      | 12 +--
 .../frontend/src/pages/user/reactions.vue     |  2 +-
 .../frontend/src/pages/welcome.entrance.a.vue |  8 +-
 packages/frontend/src/pages/welcome.setup.vue |  4 +-
 .../src/pages/welcome.timeline.note.vue       |  4 +-
 packages/frontend/src/scripts/init-chart.ts   |  2 +-
 packages/frontend/src/scripts/theme.ts        |  2 +-
 packages/frontend/src/style.scss              | 88 +++++++++----------
 .../src/ui/_common_/announcements.vue         | 12 +--
 packages/frontend/src/ui/_common_/common.vue  |  4 +-
 .../src/ui/_common_/navbar-for-mobile.vue     | 20 ++---
 packages/frontend/src/ui/_common_/navbar.vue  | 54 ++++++------
 .../frontend/src/ui/_common_/statusbars.vue   |  4 +-
 packages/frontend/src/ui/_common_/upload.vue  |  4 +-
 packages/frontend/src/ui/classic.header.vue   | 10 +--
 packages/frontend/src/ui/classic.sidebar.vue  | 10 +--
 packages/frontend/src/ui/classic.vue          | 14 +--
 packages/frontend/src/ui/deck.vue             | 26 +++---
 packages/frontend/src/ui/deck/column.vue      | 32 +++----
 .../frontend/src/ui/deck/widgets-column.vue   |  2 +-
 packages/frontend/src/ui/universal.vue        | 36 ++++----
 packages/frontend/src/ui/visitor.vue          | 12 +--
 packages/frontend/src/ui/zen.vue              |  4 +-
 .../frontend/src/widgets/WidgetAiscript.vue   |  6 +-
 .../frontend/src/widgets/WidgetCalendar.vue   |  2 +-
 .../frontend/src/widgets/WidgetFederation.vue |  4 +-
 .../frontend/src/widgets/WidgetJobQueue.vue   |  8 +-
 packages/frontend/src/widgets/WidgetMemo.vue  |  4 +-
 .../src/widgets/WidgetOnlineUsers.vue         |  2 +-
 packages/frontend/src/widgets/WidgetRss.vue   |  2 +-
 .../frontend/src/widgets/WidgetRssTicker.vue  |  4 +-
 .../frontend/src/widgets/WidgetTrends.vue     |  4 +-
 280 files changed, 1076 insertions(+), 1093 deletions(-)

diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index f61311f1e5..3a4dc7b918 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -578,18 +578,18 @@ ESMではディレクトリインポートは廃止されているのと、デ
 ### Lighten CSS vars
 
 ``` css
-color: hsl(from var(--accent) h s calc(l + 10));
+color: hsl(from var(--MI_THEME-accent) h s calc(l + 10));
 ```
 
 ### Darken CSS vars
 
 ``` css
-color: hsl(from var(--accent) h s calc(l - 10));
+color: hsl(from var(--MI_THEME-accent) h s calc(l - 10));
 ```
 
 ### Add alpha to CSS vars
 
 ``` css
-color: color(from var(--accent) srgb r g b / 0.5);
+color: color(from var(--MI_THEME-accent) srgb r g b / 0.5);
 ```
 
diff --git a/idea/MkDisableSection.vue b/idea/MkDisableSection.vue
index d177886569..360705071b 100644
--- a/idea/MkDisableSection.vue
+++ b/idea/MkDisableSection.vue
@@ -34,7 +34,7 @@ defineProps<{
 	width: 100%;
 	height: 100%;
 	cursor: not-allowed;
-	--color: color(from var(--error) srgb r g b / 0.25);
+	--color: color(from var(--MI_THEME-error) srgb r g b / 0.25);
 	background-size: auto auto;
 	background-image: repeating-linear-gradient(135deg, transparent, transparent 10px, var(--color) 4px, var(--color) 14px);
 }
diff --git a/locales/index.d.ts b/locales/index.d.ts
index d502c5b432..f0dead1245 100644
--- a/locales/index.d.ts
+++ b/locales/index.d.ts
@@ -7705,10 +7705,6 @@ export interface Locale extends ILocale {
              * 入力ボックスの縁取り
              */
             "inputBorder": string;
-            /**
-             * リスト項目の背景 (ホバー)
-             */
-            "listItemHoverBg": string;
             /**
              * ドライブフォルダーの背景
              */
diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml
index 678bc7e66b..0076c467ec 100644
--- a/locales/ja-JP.yml
+++ b/locales/ja-JP.yml
@@ -2018,7 +2018,6 @@ _theme:
     buttonBg: "ボタンの背景"
     buttonHoverBg: "ボタンの背景 (ホバー)"
     inputBorder: "入力ボックスの縁取り"
-    listItemHoverBg: "リスト項目の背景 (ホバー)"
     driveFolderBg: "ドライブフォルダーの背景"
     wallpaperOverlay: "壁紙のオーバーレイ"
     badge: "バッジ"
diff --git a/packages/backend/src/server/web/boot.js b/packages/backend/src/server/web/boot.js
index 7c6a533429..a04640d993 100644
--- a/packages/backend/src/server/web/boot.js
+++ b/packages/backend/src/server/web/boot.js
@@ -98,7 +98,7 @@
 	const theme = localStorage.getItem('theme');
 	if (theme) {
 		for (const [k, v] of Object.entries(JSON.parse(theme))) {
-			document.documentElement.style.setProperty(`--${k}`, v.toString());
+			document.documentElement.style.setProperty(`--MI_THEME-${k}`, v.toString());
 
 			// HTMLの theme-color 適用
 			if (k === 'htmlThemeColor') {
diff --git a/packages/backend/src/server/web/style.css b/packages/backend/src/server/web/style.css
index dbcc8f537c..5d81f2bed0 100644
--- a/packages/backend/src/server/web/style.css
+++ b/packages/backend/src/server/web/style.css
@@ -5,8 +5,8 @@
  */
 
 html {
-	background-color: var(--bg);
-	color: var(--fg);
+	background-color: var(--MI_THEME-bg);
+	color: var(--MI_THEME-fg);
 }
 
 #splash {
@@ -17,7 +17,7 @@ html {
 	width: 100vw;
 	height: 100vh;
 	cursor: wait;
-	background-color: var(--bg);
+	background-color: var(--MI_THEME-bg);
 	opacity: 1;
 	transition: opacity 0.5s ease;
 }
@@ -45,7 +45,7 @@ html {
 	width: 28px;
 	height: 28px;
 	transform: translateY(70px);
-	color: var(--accent);
+	color: var(--MI_THEME-accent);
 }
 
 #splashSpinner > .spinner {
diff --git a/packages/backend/src/server/web/style.embed.css b/packages/backend/src/server/web/style.embed.css
index a7b110d80a..5e8786cc4e 100644
--- a/packages/backend/src/server/web/style.embed.css
+++ b/packages/backend/src/server/web/style.embed.css
@@ -5,8 +5,8 @@
  */
 
 html {
-	background-color: var(--bg);
-	color: var(--fg);
+	background-color: var(--MI_THEME-bg);
+	color: var(--MI_THEME-fg);
 }
 
 html.embed {
@@ -24,7 +24,7 @@ html.embed {
 	width: 100vw;
 	height: 100vh;
 	cursor: wait;
-	background-color: var(--bg);
+	background-color: var(--MI_THEME-bg);
 	opacity: 1;
 	transition: opacity 0.5s ease;
 }
@@ -33,7 +33,7 @@ html.embed #splash {
 	box-sizing: border-box;
 	min-height: 300px;
 	border-radius: var(--radius, 12px);
-	border: 1px solid var(--divider, #e8e8e8);
+	border: 1px solid var(--MI_THEME-divider, #e8e8e8);
 }
 
 html.embed.norounded #splash {
@@ -67,7 +67,7 @@ html.embed.noborder #splash {
 	width: 28px;
 	height: 28px;
 	transform: translateY(70px);
-	color: var(--accent);
+	color: var(--MI_THEME-accent);
 }
 
 #splashSpinner > .spinner {
diff --git a/packages/frontend-embed/src/components/EmLoading.vue b/packages/frontend-embed/src/components/EmLoading.vue
index 49d8ace37b..47d797606b 100644
--- a/packages/frontend-embed/src/components/EmLoading.vue
+++ b/packages/frontend-embed/src/components/EmLoading.vue
@@ -56,7 +56,7 @@ const props = withDefaults(defineProps<{
 	--size: 38px;
 
 	&.colored {
-		color: var(--accent);
+		color: var(--MI_THEME-accent);
 	}
 
 	&.inline {
diff --git a/packages/frontend-embed/src/components/EmMediaBanner.vue b/packages/frontend-embed/src/components/EmMediaBanner.vue
index 435da238a4..3e3dfd95b2 100644
--- a/packages/frontend-embed/src/components/EmMediaBanner.vue
+++ b/packages/frontend-embed/src/components/EmMediaBanner.vue
@@ -33,15 +33,15 @@ defineProps<{
 	width: 100%;
 	padding: var(--margin);
 	margin-top: 4px;
-	border: 1px solid var(--inputBorder);
+	border: 1px solid var(--MI_THEME-inputBorder);
 	border-radius: var(--radius);
-	background-color: var(--panel);
+	background-color: var(--MI_THEME-panel);
 	transition: background-color .1s, border-color .1s;
 
 	&:hover {
 		text-decoration: none;
-		border-color: var(--inputBorderHover);
-		background-color: var(--buttonHoverBg);
+		border-color: var(--MI_THEME-inputBorderHover);
+		background-color: var(--MI_THEME-buttonHoverBg);
 	}
 }
 
diff --git a/packages/frontend-embed/src/components/EmMediaImage.vue b/packages/frontend-embed/src/components/EmMediaImage.vue
index 470352469b..d711020a74 100644
--- a/packages/frontend-embed/src/components/EmMediaImage.vue
+++ b/packages/frontend-embed/src/components/EmMediaImage.vue
@@ -36,7 +36,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 	<div :class="$style.indicators">
 		<div v-if="['image/gif', 'image/apng'].includes(image.type)" :class="$style.indicator">GIF</div>
 		<div v-if="image.comment" :class="$style.indicator">ALT</div>
-		<div v-if="image.isSensitive" :class="$style.indicator" style="color: var(--warn);" :title="i18n.ts.sensitive"><i class="ti ti-eye-exclamation"></i></div>
+		<div v-if="image.isSensitive" :class="$style.indicator" style="color: var(--MI_THEME-warn);" :title="i18n.ts.sensitive"><i class="ti ti-eye-exclamation"></i></div>
 	</div>
 	<i v-if="!hide" class="ti ti-eye-off" :class="$style.hide" @click.stop="hide = true"></i>
 </div>
@@ -94,8 +94,8 @@ async function onclick(ev: MouseEvent) {
 	display: block;
 	position: absolute;
 	border-radius: 6px;
-	background-color: var(--fg);
-	color: var(--accentLighten);
+	background-color: var(--MI_THEME-fg);
+	color: var(--MI_THEME-accentLighten);
 	font-size: 12px;
 	opacity: .5;
 	padding: 5px 8px;
@@ -114,19 +114,19 @@ async function onclick(ev: MouseEvent) {
 
 .visible {
 	position: relative;
-	//box-shadow: 0 0 0 1px var(--divider) inset;
-	background: var(--bg);
+	//box-shadow: 0 0 0 1px var(--MI_THEME-divider) inset;
+	background: var(--MI_THEME-bg);
 	background-size: 16px 16px;
 }
 
 html[data-color-scheme=dark] .visible {
 	--c: rgb(255 255 255 / 2%);
-	background-image: linear-gradient(45deg, var(--c) 16.67%, var(--bg) 16.67%, var(--bg) 50%, var(--c) 50%, var(--c) 66.67%, var(--bg) 66.67%, var(--bg) 100%);
+	background-image: linear-gradient(45deg, var(--c) 16.67%, var(--MI_THEME-bg) 16.67%, var(--MI_THEME-bg) 50%, var(--c) 50%, var(--c) 66.67%, var(--MI_THEME-bg) 66.67%, var(--MI_THEME-bg) 100%);
 }
 
 html[data-color-scheme=light] .visible {
 	--c: rgb(0 0 0 / 2%);
-	background-image: linear-gradient(45deg, var(--c) 16.67%, var(--bg) 16.67%, var(--bg) 50%, var(--c) 50%, var(--c) 66.67%, var(--bg) 66.67%, var(--bg) 100%);
+	background-image: linear-gradient(45deg, var(--c) 16.67%, var(--MI_THEME-bg) 16.67%, var(--MI_THEME-bg) 50%, var(--c) 50%, var(--c) 66.67%, var(--MI_THEME-bg) 66.67%, var(--MI_THEME-bg) 100%);
 }
 
 .imageContainer {
@@ -150,10 +150,10 @@ html[data-color-scheme=light] .visible {
 }
 
 .indicator {
-	/* Hardcode to black because either --bg or --fg makes it hard to read in dark/light mode */
+	/* Hardcode to black because either --MI_THEME-bg or --MI_THEME-fg makes it hard to read in dark/light mode */
 	background-color: black;
 	border-radius: 6px;
-	color: var(--accentLighten);
+	color: var(--MI_THEME-accentLighten);
 	display: inline-block;
 	font-weight: bold;
 	font-size: 0.8em;
diff --git a/packages/frontend-embed/src/components/EmMediaVideo.vue b/packages/frontend-embed/src/components/EmMediaVideo.vue
index ce751f9acd..5ca0b92d43 100644
--- a/packages/frontend-embed/src/components/EmMediaVideo.vue
+++ b/packages/frontend-embed/src/components/EmMediaVideo.vue
@@ -30,7 +30,7 @@ defineProps<{
 	height: auto;
 	aspect-ratio: 16 / 9;
 	padding: var(--margin);
-	border: 1px solid var(--divider);
+	border: 1px solid var(--MI_THEME-divider);
 	border-radius: var(--radius);
 	background-color: #000;
 
@@ -49,7 +49,7 @@ defineProps<{
 }
 
 .videoOverlayPlayButton {
-	background: var(--accent);
+	background: var(--MI_THEME-accent);
 	color: #fff;
 	padding: 1rem;
 	border-radius: 99rem;
diff --git a/packages/frontend-embed/src/components/EmMention.vue b/packages/frontend-embed/src/components/EmMention.vue
index a631783507..a71364237d 100644
--- a/packages/frontend-embed/src/components/EmMention.vue
+++ b/packages/frontend-embed/src/components/EmMention.vue
@@ -27,7 +27,7 @@ const canonical = props.host === localHost ? `@${props.username}` : `@${props.us
 
 const url = `/${canonical}`;
 
-const bg = tinycolor(getComputedStyle(document.documentElement).getPropertyValue('--mention'));
+const bg = tinycolor(getComputedStyle(document.documentElement).getPropertyValue('--MI_THEME-mention'));
 bg.setAlpha(0.1);
 const bgCss = bg.toRgbString();
 </script>
@@ -37,7 +37,7 @@ const bgCss = bg.toRgbString();
 	display: inline-block;
 	padding: 4px 8px 4px 4px;
 	border-radius: 999px;
-	color: var(--mention);
+	color: var(--MI_THEME-mention);
 }
 
 .host {
diff --git a/packages/frontend-embed/src/components/EmMfm.ts b/packages/frontend-embed/src/components/EmMfm.ts
index 59f0d495e6..cae2feb8fb 100644
--- a/packages/frontend-embed/src/components/EmMfm.ts
+++ b/packages/frontend-embed/src/components/EmMfm.ts
@@ -26,8 +26,8 @@ const QUOTE_STYLE = `
 display: block;
 margin: 8px;
 padding: 6px 0 6px 12px;
-color: var(--fg);
-border-left: solid 3px var(--fg);
+color: var(--MI_THEME-fg);
+border-left: solid 3px var(--MI_THEME-fg);
 opacity: 0.7;
 `.split('\n').join(' ');
 
@@ -251,7 +251,7 @@ export default function (props: MfmProps, { emit }: { emit: SetupContext<MfmEven
 					}
 					case 'border': {
 						let color = validColor(token.props.args.color);
-						color = color ? `#${color}` : 'var(--accent)';
+						color = color ? `#${color}` : 'var(--MI_THEME-accent)';
 						let b_style = token.props.args.style;
 						if (
 							typeof b_style !== 'string' ||
@@ -284,7 +284,7 @@ export default function (props: MfmProps, { emit }: { emit: SetupContext<MfmEven
 						const child = token.children[0];
 						const unixtime = parseInt(child.type === 'text' ? child.props.text : '');
 						return h('span', {
-							style: 'display: inline-block; font-size: 90%; border: solid 1px var(--divider); border-radius: 999px; padding: 4px 10px 4px 6px;',
+							style: 'display: inline-block; font-size: 90%; border: solid 1px var(--MI_THEME-divider); border-radius: 999px; padding: 4px 10px 4px 6px;',
 						}, [
 							h('i', {
 								class: 'ti ti-clock',
@@ -355,7 +355,7 @@ export default function (props: MfmProps, { emit }: { emit: SetupContext<MfmEven
 				return [h(EmA, {
 					key: Math.random(),
 					to: isNote ? `/tags/${encodeURIComponent(token.props.hashtag)}` : `/user-tags/${encodeURIComponent(token.props.hashtag)}`,
-					style: 'color:var(--hashtag);',
+					style: 'color:var(--MI_THEME-hashtag);',
 				}, `#${token.props.hashtag}`)];
 			}
 
diff --git a/packages/frontend-embed/src/components/EmNote.vue b/packages/frontend-embed/src/components/EmNote.vue
index f7899bfb03..7eeeda1797 100644
--- a/packages/frontend-embed/src/components/EmNote.vue
+++ b/packages/frontend-embed/src/components/EmNote.vue
@@ -189,7 +189,7 @@ const isDeleted = ref(false);
 			margin: auto;
 			width: calc(100% - 8px);
 			height: calc(100% - 8px);
-			border: dashed 2px var(--focus);
+			border: dashed 2px var(--MI_THEME-focus);
 			border-radius: var(--radius);
 			box-sizing: border-box;
 		}
@@ -212,9 +212,9 @@ const isDeleted = ref(false);
 			right: 12px;
 			padding: 0 4px;
 			margin-bottom: 0 !important;
-			background: var(--popup);
+			background: var(--MI_THEME-popup);
 			border-radius: 8px;
-			box-shadow: 0px 4px 32px var(--shadow);
+			box-shadow: 0px 4px 32px var(--MI_THEME-shadow);
 		}
 
 		.footerButton {
@@ -259,7 +259,7 @@ const isDeleted = ref(false);
 	padding: 16px 32px 8px 32px;
 	line-height: 28px;
 	white-space: pre;
-	color: var(--renote);
+	color: var(--MI_THEME-renote);
 
 	& + .article {
 		padding-top: 8px;
@@ -382,7 +382,7 @@ const isDeleted = ref(false);
 
 .showLessLabel {
 	display: inline-block;
-	background: var(--popup);
+	background: var(--MI_THEME-popup);
 	padding: 6px 10px;
 	font-size: 0.8em;
 	border-radius: 999px;
@@ -403,16 +403,16 @@ const isDeleted = ref(false);
 	z-index: 2;
 	width: 100%;
 	height: 64px;
-	background: linear-gradient(0deg, var(--panel), color(from var(--panel) srgb r g b / 0));
+	background: linear-gradient(0deg, var(--MI_THEME-panel), color(from var(--MI_THEME-panel) srgb r g b / 0));
 
 	&:hover > .collapsedLabel {
-		background: var(--panelHighlight);
+		background: var(--MI_THEME-panelHighlight);
 	}
 }
 
 .collapsedLabel {
 	display: inline-block;
-	background: var(--panel);
+	background: var(--MI_THEME-panel);
 	padding: 6px 10px;
 	font-size: 0.8em;
 	border-radius: 999px;
@@ -424,12 +424,12 @@ const isDeleted = ref(false);
 }
 
 .replyIcon {
-	color: var(--accent);
+	color: var(--MI_THEME-accent);
 	margin-right: 0.5em;
 }
 
 .translation {
-	border: solid 0.5px var(--divider);
+	border: solid 0.5px var(--MI_THEME-divider);
 	border-radius: var(--radius);
 	padding: 12px;
 	margin-top: 8px;
@@ -449,7 +449,7 @@ const isDeleted = ref(false);
 
 .quoteNote {
 	padding: 16px;
-	border: dashed 1px var(--renote);
+	border: dashed 1px var(--MI_THEME-renote);
 	border-radius: 8px;
 	overflow: clip;
 }
@@ -473,7 +473,7 @@ const isDeleted = ref(false);
 	}
 
 	&:hover {
-		color: var(--fgHighlighted);
+		color: var(--MI_THEME-fgHighlighted);
 	}
 }
 
diff --git a/packages/frontend-embed/src/components/EmNoteDetailed.vue b/packages/frontend-embed/src/components/EmNoteDetailed.vue
index 360de31864..ccd723d7d2 100644
--- a/packages/frontend-embed/src/components/EmNoteDetailed.vue
+++ b/packages/frontend-embed/src/components/EmNoteDetailed.vue
@@ -195,7 +195,7 @@ const collapsed = ref(appearNote.value.cw == null && isLong);
 	padding: 16px 32px 8px 32px;
 	line-height: 28px;
 	white-space: pre;
-	color: var(--renote);
+	color: var(--MI_THEME-renote);
 }
 
 .renoteAvatar {
@@ -281,7 +281,7 @@ const collapsed = ref(appearNote.value.cw == null && isLong);
 	padding: 4px 6px;
 	font-size: 80%;
 	line-height: 1;
-	border: solid 0.5px var(--divider);
+	border: solid 0.5px var(--MI_THEME-divider);
 	border-radius: 4px;
 }
 
@@ -323,14 +323,14 @@ const collapsed = ref(appearNote.value.cw == null && isLong);
 }
 
 .noteReplyTarget {
-	color: var(--accent);
+	color: var(--MI_THEME-accent);
 	margin-right: 0.5em;
 }
 
 .rn {
 	margin-left: 4px;
 	font-style: oblique;
-	color: var(--renote);
+	color: var(--MI_THEME-renote);
 }
 
 .reactionOmitted {
@@ -350,7 +350,7 @@ const collapsed = ref(appearNote.value.cw == null && isLong);
 
 .quoteNote {
 	padding: 16px;
-	border: dashed 1px var(--renote);
+	border: dashed 1px var(--MI_THEME-renote);
 	border-radius: 8px;
 	overflow: clip;
 }
@@ -369,7 +369,7 @@ const collapsed = ref(appearNote.value.cw == null && isLong);
 
 .showLessLabel {
 	display: inline-block;
-	background: var(--popup);
+	background: var(--MI_THEME-popup);
 	padding: 6px 10px;
 	font-size: 0.8em;
 	border-radius: 999px;
@@ -390,16 +390,16 @@ const collapsed = ref(appearNote.value.cw == null && isLong);
 	z-index: 2;
 	width: 100%;
 	height: 64px;
-	background: linear-gradient(0deg, var(--panel), var(--X15));
+	background: linear-gradient(0deg, var(--MI_THEME-panel), var(--MI_THEME-X15));
 
 	&:hover > .collapsedLabel {
-		background: var(--panelHighlight);
+		background: var(--MI_THEME-panelHighlight);
 	}
 }
 
 .collapsedLabel {
 	display: inline-block;
-	background: var(--panel);
+	background: var(--MI_THEME-panel);
 	padding: 6px 10px;
 	font-size: 0.8em;
 	border-radius: 999px;
@@ -422,7 +422,7 @@ const collapsed = ref(appearNote.value.cw == null && isLong);
 	}
 
 	&:hover {
-		color: var(--fgHighlighted);
+		color: var(--MI_THEME-fgHighlighted);
 	}
 }
 
@@ -438,7 +438,7 @@ const collapsed = ref(appearNote.value.cw == null && isLong);
 	opacity: 0.7;
 
 	&.reacted {
-		color: var(--accent);
+		color: var(--MI_THEME-accent);
 	}
 }
 
diff --git a/packages/frontend-embed/src/components/EmNoteHeader.vue b/packages/frontend-embed/src/components/EmNoteHeader.vue
index 7d0b9bacad..85b4aac071 100644
--- a/packages/frontend-embed/src/components/EmNoteHeader.vue
+++ b/packages/frontend-embed/src/components/EmNoteHeader.vue
@@ -72,7 +72,7 @@ defineProps<{
 	margin: 0 .5em 0 0;
 	padding: 1px 6px;
 	font-size: 80%;
-	border: solid 0.5px var(--divider);
+	border: solid 0.5px var(--MI_THEME-divider);
 	border-radius: 3px;
 }
 
diff --git a/packages/frontend-embed/src/components/EmNoteSub.vue b/packages/frontend-embed/src/components/EmNoteSub.vue
index f60aea3e7e..59be8608e0 100644
--- a/packages/frontend-embed/src/components/EmNoteSub.vue
+++ b/packages/frontend-embed/src/components/EmNoteSub.vue
@@ -123,7 +123,7 @@ if (props.detail) {
 }
 
 .reply, .more {
-	border-left: solid 0.5px var(--divider);
+	border-left: solid 0.5px var(--MI_THEME-divider);
 	margin-top: 10px;
 }
 
@@ -144,7 +144,7 @@ if (props.detail) {
 .muted {
 	text-align: center;
 	padding: 8px !important;
-	border: 1px solid var(--divider);
+	border: 1px solid var(--MI_THEME-divider);
 	margin: 8px 8px 0 8px;
 	border-radius: 8px;
 }
diff --git a/packages/frontend-embed/src/components/EmNotes.vue b/packages/frontend-embed/src/components/EmNotes.vue
index 3418d97f77..4211261e19 100644
--- a/packages/frontend-embed/src/components/EmNotes.vue
+++ b/packages/frontend-embed/src/components/EmNotes.vue
@@ -43,10 +43,10 @@ defineExpose({
 
 <style lang="scss" module>
 .root {
-	background: var(--panel);
+	background: var(--MI_THEME-panel);
 }
 
 .note {
-	border-bottom: 0.5px solid var(--divider);
+	border-bottom: 0.5px solid var(--MI_THEME-divider);
 }
 </style>
diff --git a/packages/frontend-embed/src/components/EmPoll.vue b/packages/frontend-embed/src/components/EmPoll.vue
index a2b1203449..d197e094c6 100644
--- a/packages/frontend-embed/src/components/EmPoll.vue
+++ b/packages/frontend-embed/src/components/EmPoll.vue
@@ -9,7 +9,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 		<li v-for="(choice, i) in poll.choices" :key="i" :class="$style.choice">
 			<div :class="$style.bg" :style="{ 'width': `${choice.votes / total * 100}%` }"></div>
 			<span :class="$style.fg">
-				<template v-if="choice.isVoted"><i class="ti ti-check" style="margin-right: 4px; color: var(--accent);"></i></template>
+				<template v-if="choice.isVoted"><i class="ti ti-check" style="margin-right: 4px; color: var(--MI_THEME-accent);"></i></template>
 				<EmMfm :text="choice.text" :plain="true"/>
 				<span style="margin-left: 4px; opacity: 0.7;">({{ i18n.tsx._poll.votesCount({ n: choice.votes }) }})</span>
 			</span>
@@ -52,8 +52,8 @@ const total = computed(() => sum(props.poll.choices.map(x => x.votes)));
 	position: relative;
 	margin: 4px 0;
 	padding: 4px;
-	//border: solid 0.5px var(--divider);
-	background: var(--accentedBg);
+	//border: solid 0.5px var(--MI_THEME-divider);
+	background: var(--MI_THEME-accentedBg);
 	border-radius: 4px;
 	overflow: clip;
 }
@@ -63,8 +63,8 @@ const total = computed(() => sum(props.poll.choices.map(x => x.votes)));
 	top: 0;
 	left: 0;
 	height: 100%;
-	background: var(--accent);
-	background: linear-gradient(90deg,var(--buttonGradateA),var(--buttonGradateB));
+	background: var(--MI_THEME-accent);
+	background: linear-gradient(90deg,var(--MI_THEME-buttonGradateA),var(--MI_THEME-buttonGradateB));
 	transition: width 1s ease;
 }
 
@@ -72,11 +72,11 @@ const total = computed(() => sum(props.poll.choices.map(x => x.votes)));
 	position: relative;
 	display: inline-block;
 	padding: 3px 5px;
-	background: var(--panel);
+	background: var(--MI_THEME-panel);
 	border-radius: 3px;
 }
 
 .info {
-	color: var(--fg);
+	color: var(--MI_THEME-fg);
 }
 </style>
diff --git a/packages/frontend-embed/src/components/EmReactionsViewer.reaction.vue b/packages/frontend-embed/src/components/EmReactionsViewer.reaction.vue
index 2e43eb8d17..2ebff489fd 100644
--- a/packages/frontend-embed/src/components/EmReactionsViewer.reaction.vue
+++ b/packages/frontend-embed/src/components/EmReactionsViewer.reaction.vue
@@ -38,7 +38,7 @@ const props = defineProps<{
 	justify-content: center;
 
 	&.canToggle {
-		background: var(--buttonBg);
+		background: var(--MI_THEME-buttonBg);
 
 		&:hover {
 			background: rgba(0, 0, 0, 0.1);
@@ -72,12 +72,12 @@ const props = defineProps<{
 	}
 
 	&.reacted, &.reacted:hover {
-		background: var(--accentedBg);
-		color: var(--accent);
-		box-shadow: 0 0 0 1px var(--accent) inset;
+		background: var(--MI_THEME-accentedBg);
+		color: var(--MI_THEME-accent);
+		box-shadow: 0 0 0 1px var(--MI_THEME-accent) inset;
 
 		> .count {
-			color: var(--accent);
+			color: var(--MI_THEME-accent);
 		}
 
 		> .icon {
diff --git a/packages/frontend-embed/src/components/EmSubNoteContent.vue b/packages/frontend-embed/src/components/EmSubNoteContent.vue
index db2666a45f..dcaa1ec914 100644
--- a/packages/frontend-embed/src/components/EmSubNoteContent.vue
+++ b/packages/frontend-embed/src/components/EmSubNoteContent.vue
@@ -65,11 +65,11 @@ const collapsed = ref(isLong);
 			left: 0;
 			width: 100%;
 			height: 64px;
-			background: linear-gradient(0deg, var(--panel), color(from var(--panel) srgb r g b / 0));
+			background: linear-gradient(0deg, var(--MI_THEME-panel), color(from var(--MI_THEME-panel) srgb r g b / 0));
 
 			> .fadeLabel {
 				display: inline-block;
-				background: var(--panel);
+				background: var(--MI_THEME-panel);
 				padding: 6px 10px;
 				font-size: 0.8em;
 				border-radius: 999px;
@@ -78,7 +78,7 @@ const collapsed = ref(isLong);
 
 			&:hover {
 				> .fadeLabel {
-					background: var(--panelHighlight);
+					background: var(--MI_THEME-panelHighlight);
 				}
 			}
 		}
@@ -87,13 +87,13 @@ const collapsed = ref(isLong);
 
 .reply {
 	margin-right: 6px;
-	color: var(--accent);
+	color: var(--MI_THEME-accent);
 }
 
 .rp {
 	margin-left: 4px;
 	font-style: oblique;
-	color: var(--renote);
+	color: var(--MI_THEME-renote);
 }
 
 .showLess {
@@ -105,7 +105,7 @@ const collapsed = ref(isLong);
 
 .showLessLabel {
 	display: inline-block;
-	background: var(--popup);
+	background: var(--MI_THEME-popup);
 	padding: 6px 10px;
 	font-size: 0.8em;
 	border-radius: 999px;
diff --git a/packages/frontend-embed/src/components/EmTime.vue b/packages/frontend-embed/src/components/EmTime.vue
index c3986f7d70..7902e18483 100644
--- a/packages/frontend-embed/src/components/EmTime.vue
+++ b/packages/frontend-embed/src/components/EmTime.vue
@@ -98,10 +98,10 @@ if (!invalid && props.origin === null && (props.mode === 'relative' || props.mod
 
 <style lang="scss" module>
 .old1 {
-	color: var(--warn);
+	color: var(--MI_THEME-warn);
 }
 
 .old1.old2 {
-	color: var(--error);
+	color: var(--MI_THEME-error);
 }
 </style>
diff --git a/packages/frontend-embed/src/components/EmTimelineContainer.vue b/packages/frontend-embed/src/components/EmTimelineContainer.vue
index 6c30b1102d..60fd67ced9 100644
--- a/packages/frontend-embed/src/components/EmTimelineContainer.vue
+++ b/packages/frontend-embed/src/components/EmTimelineContainer.vue
@@ -20,7 +20,7 @@ withDefaults(defineProps<{
 
 <style module lang="scss">
 .timelineRoot {
-	background-color: var(--panel);
+	background-color: var(--MI_THEME-panel);
 	height: 100%;
 	max-height: var(--embedMaxHeight, none);
 	display: flex;
@@ -29,7 +29,7 @@ withDefaults(defineProps<{
 
 .header {
 	flex-shrink: 0;
-	border-bottom: 1px solid var(--divider);
+	border-bottom: 1px solid var(--MI_THEME-divider);
 }
 
 .body {
diff --git a/packages/frontend-embed/src/pages/clip.vue b/packages/frontend-embed/src/pages/clip.vue
index 2528dc4b80..d805cb3e4f 100644
--- a/packages/frontend-embed/src/pages/clip.vue
+++ b/packages/frontend-embed/src/pages/clip.vue
@@ -110,8 +110,8 @@ function top(ev: MouseEvent) {
 		line-height: 32px;
 		font-size: 14px;
 		text-align: center;
-		background-color: var(--accentedBg);
-		color: var(--accent);
+		background-color: var(--MI_THEME-accentedBg);
+		color: var(--MI_THEME-accent);
 		border-radius: 50%;
 	}
 
diff --git a/packages/frontend-embed/src/pages/note.vue b/packages/frontend-embed/src/pages/note.vue
index 6f6c8c0f63..e879430286 100644
--- a/packages/frontend-embed/src/pages/note.vue
+++ b/packages/frontend-embed/src/pages/note.vue
@@ -47,6 +47,6 @@ if (note.value?.url != null || note.value?.uri != null) {
 
 <style lang="scss" module>
 .noteEmbedRoot {
-	background-color: var(--panel);
+	background-color: var(--MI_THEME-panel);
 }
 </style>
diff --git a/packages/frontend-embed/src/pages/tag.vue b/packages/frontend-embed/src/pages/tag.vue
index b481b3ebe5..78049e4041 100644
--- a/packages/frontend-embed/src/pages/tag.vue
+++ b/packages/frontend-embed/src/pages/tag.vue
@@ -93,8 +93,8 @@ function top(ev: MouseEvent) {
 		line-height: 32px;
 		font-size: 14px;
 		text-align: center;
-		background-color: var(--accentedBg);
-		color: var(--accent);
+		background-color: var(--MI_THEME-accentedBg);
+		color: var(--MI_THEME-accent);
 		border-radius: 50%;
 	}
 
diff --git a/packages/frontend-embed/src/style.scss b/packages/frontend-embed/src/style.scss
index 02008ddbd0..1569de01f8 100644
--- a/packages/frontend-embed/src/style.scss
+++ b/packages/frontend-embed/src/style.scss
@@ -17,8 +17,8 @@
 html {
 	background-color: transparent;
 	color-scheme: light dark;
-	color: var(--fg);
-	accent-color: var(--accent);
+	color: var(--MI_THEME-fg);
+	accent-color: var(--MI_THEME-accent);
 	overflow: clip;
 	overflow-wrap: break-word;
 	font-family: 'Hiragino Maru Gothic Pro', "BIZ UDGothic", Roboto, HelveticaNeue, Arial, sans-serif;
@@ -29,7 +29,7 @@ html {
 	-webkit-text-size-adjust: 100%;
 
 	&, * {
-		scrollbar-color: var(--scrollbarHandle) transparent;
+		scrollbar-color: var(--MI_THEME-scrollbarHandle) transparent;
 		scrollbar-width: thin;
 
 		&::-webkit-scrollbar {
@@ -42,14 +42,14 @@ html {
 		}
 
 		&::-webkit-scrollbar-thumb {
-			background: var(--scrollbarHandle);
+			background: var(--MI_THEME-scrollbarHandle);
 
 			&:hover {
-				background: var(--scrollbarHandleHover);
+				background: var(--MI_THEME-scrollbarHandleHover);
 			}
 
 			&:active {
-				background: var(--accent);
+				background: var(--MI_THEME-accent);
 			}
 		}
 	}
@@ -93,7 +93,7 @@ rt {
 }
 
 :focus-visible {
-	outline: var(--focus) solid 2px;
+	outline: var(--MI_THEME-focus) solid 2px;
 	outline-offset: -2px;
 
 	&:hover {
@@ -151,38 +151,38 @@ rt {
 
 ._buttonGray {
 	@extend ._button;
-	background: var(--buttonBg);
+	background: var(--MI_THEME-buttonBg);
 
 	&:not(:disabled):hover {
-		background: var(--buttonHoverBg);
+		background: var(--MI_THEME-buttonHoverBg);
 	}
 }
 
 ._buttonPrimary {
 	@extend ._button;
-	color: var(--fgOnAccent);
-	background: var(--accent);
+	color: var(--MI_THEME-fgOnAccent);
+	background: var(--MI_THEME-accent);
 
 	&:not(:disabled):hover {
-		background: hsl(from var(--accent) h s calc(l + 5));
+		background: hsl(from var(--MI_THEME-accent) h s calc(l + 5));
 	}
 
 	&:not(:disabled):active {
-		background: hsl(from var(--accent) h s calc(l - 5));
+		background: hsl(from var(--MI_THEME-accent) h s calc(l - 5));
 	}
 }
 
 ._buttonGradate {
 	@extend ._buttonPrimary;
-	color: var(--fgOnAccent);
-	background: linear-gradient(90deg, var(--buttonGradateA), var(--buttonGradateB));
+	color: var(--MI_THEME-fgOnAccent);
+	background: linear-gradient(90deg, var(--MI_THEME-buttonGradateA), var(--MI_THEME-buttonGradateB));
 
 	&:not(:disabled):hover {
-		background: linear-gradient(90deg, hsl(from var(--accent) h s calc(l + 5)), hsl(from var(--accent) h s calc(l + 5)));
+		background: linear-gradient(90deg, hsl(from var(--MI_THEME-accent) h s calc(l + 5)), hsl(from var(--MI_THEME-accent) h s calc(l + 5)));
 	}
 
 	&:not(:disabled):active {
-		background: linear-gradient(90deg, hsl(from var(--accent) h s calc(l + 5)), hsl(from var(--accent) h s calc(l + 5)));
+		background: linear-gradient(90deg, hsl(from var(--MI_THEME-accent) h s calc(l + 5)), hsl(from var(--MI_THEME-accent) h s calc(l + 5)));
 	}
 }
 
@@ -199,13 +199,13 @@ rt {
 }
 
 ._help {
-	color: var(--accent);
+	color: var(--MI_THEME-accent);
 	cursor: help;
 }
 
 ._textButton {
 	@extend ._button;
-	color: var(--accent);
+	color: var(--MI_THEME-accent);
 
 	&:focus-visible {
 		outline-offset: 2px;
@@ -217,7 +217,7 @@ rt {
 }
 
 ._panel {
-	background: var(--panel);
+	background: var(--MI_THEME-panel);
 	border-radius: var(--radius);
 	overflow: clip;
 }
@@ -263,22 +263,22 @@ rt {
 	padding: 10px;
 	box-sizing: border-box;
 	text-align: center;
-	border: solid 0.5px var(--divider);
+	border: solid 0.5px var(--MI_THEME-divider);
 	border-radius: var(--radius);
 
 	&:active {
-		border-color: var(--accent);
+		border-color: var(--MI_THEME-accent);
 	}
 }
 
 ._popup {
-	background: var(--popup);
+	background: var(--MI_THEME-popup);
 	border-radius: var(--radius);
 	contain: content;
 }
 
 ._acrylic {
-	background: var(--acrylicPanel);
+	background: var(--MI_THEME-acrylicPanel);
 	-webkit-backdrop-filter: var(--blur, blur(15px));
 	backdrop-filter: var(--blur, blur(15px));
 }
@@ -296,7 +296,7 @@ rt {
 }
 
 ._link {
-	color: var(--link);
+	color: var(--MI_THEME-link);
 }
 
 ._caption {
diff --git a/packages/frontend-embed/src/theme.ts b/packages/frontend-embed/src/theme.ts
index 23e70cd0d3..4664ad4880 100644
--- a/packages/frontend-embed/src/theme.ts
+++ b/packages/frontend-embed/src/theme.ts
@@ -61,7 +61,7 @@ export function applyTheme(theme: Theme, persist = true) {
 	}
 
 	for (const [k, v] of Object.entries(props)) {
-		document.documentElement.style.setProperty(`--${k}`, v.toString());
+		document.documentElement.style.setProperty(`--MI_THEME-${k}`, v.toString());
 	}
 
 	// iframeを正常に透過させるために、cssのcolor-schemeは `light dark;` 固定にしてある。style.scss参照
diff --git a/packages/frontend-embed/src/ui.vue b/packages/frontend-embed/src/ui.vue
index 8da5f46a96..2ed2f58376 100644
--- a/packages/frontend-embed/src/ui.vue
+++ b/packages/frontend-embed/src/ui.vue
@@ -88,8 +88,8 @@ onUnmounted(() => {
 <style lang="scss" module>
 .rootForEmbedPage {
 	box-sizing: border-box;
-	border: 1px solid var(--divider);
-	background-color: var(--bg);
+	border: 1px solid var(--MI_THEME-divider);
+	background-color: var(--MI_THEME-bg);
 	overflow: hidden;
 	position: relative;
 	height: auto;
diff --git a/packages/frontend-shared/themes/_dark.json5 b/packages/frontend-shared/themes/_dark.json5
index eb5fda3dc0..5271785e62 100644
--- a/packages/frontend-shared/themes/_dark.json5
+++ b/packages/frontend-shared/themes/_dark.json5
@@ -30,7 +30,7 @@
 		panelHeaderBg: ':lighten<3<@panel',
 		panelHeaderFg: '@fg',
 		panelHeaderDivider: 'rgba(0, 0, 0, 0)',
-		panelBorder: '" solid 1px var(--divider)',
+		panelBorder: '" solid 1px var(--MI_THEME-divider)',
 		acrylicPanel: ':alpha<0.5<@panel',
 		windowHeader: ':alpha<0.85<@panel',
 		popup: ':lighten<3<@panel',
@@ -67,7 +67,6 @@
 		switchOnFg: '@accent',
 		inputBorder: 'rgba(255, 255, 255, 0.1)',
 		inputBorderHover: 'rgba(255, 255, 255, 0.2)',
-		listItemHoverBg: 'rgba(255, 255, 255, 0.03)',
 		driveFolderBg: ':alpha<0.3<@accent',
 		wallpaperOverlay: 'rgba(0, 0, 0, 0.5)',
 		badge: '#31b1ce',
diff --git a/packages/frontend-shared/themes/_light.json5 b/packages/frontend-shared/themes/_light.json5
index e0196dcbf3..be331ce58f 100644
--- a/packages/frontend-shared/themes/_light.json5
+++ b/packages/frontend-shared/themes/_light.json5
@@ -30,7 +30,7 @@
 		panelHeaderBg: ':lighten<3<@panel',
 		panelHeaderFg: '@fg',
 		panelHeaderDivider: 'rgba(0, 0, 0, 0)',
-		panelBorder: '" solid 1px var(--divider)',
+		panelBorder: '" solid 1px var(--MI_THEME-divider)',
 		acrylicPanel: ':alpha<0.5<@panel',
 		windowHeader: ':alpha<0.85<@panel',
 		popup: ':lighten<3<@panel',
@@ -67,7 +67,6 @@
 		switchOnFg: '@fgOnAccent',
 		inputBorder: 'rgba(0, 0, 0, 0.1)',
 		inputBorderHover: 'rgba(0, 0, 0, 0.2)',
-		listItemHoverBg: 'rgba(0, 0, 0, 0.03)',
 		driveFolderBg: ':alpha<0.3<@accent',
 		wallpaperOverlay: 'rgba(255, 255, 255, 0.5)',
 		badge: '#31b1ce',
diff --git a/packages/frontend-shared/themes/d-astro.json5 b/packages/frontend-shared/themes/d-astro.json5
index a674a5c5c9..4422526a33 100644
--- a/packages/frontend-shared/themes/d-astro.json5
+++ b/packages/frontend-shared/themes/d-astro.json5
@@ -36,7 +36,7 @@
 		dateLabelFg: '@fg',
 		inputBorder: 'rgba(255, 255, 255, 0.1)',
 		inputBorderHover: 'rgba(255, 255, 255, 0.2)',
-		panelBorder: '" solid 1px var(--divider)',
+		panelBorder: '" solid 1px var(--MI_THEME-divider)',
 		accentDarken: ':darken<10<@accent',
 		acrylicPanel: ':alpha<0.5<@panel',
 		navIndicator: '@accent',
@@ -50,7 +50,6 @@
 		htmlThemeColor: '@bg',
 		fgOnWhite: '@accent',
 		panelHighlight: ':lighten<3<@panel',
-		listItemHoverBg: 'rgba(255, 255, 255, 0.03)',
 		scrollbarHandle: 'rgba(255, 255, 255, 0.2)',
 		wallpaperOverlay: 'rgba(0, 0, 0, 0.5)',
 		panelHeaderDivider: 'rgba(0, 0, 0, 0)',
diff --git a/packages/frontend-shared/themes/d-u0.json5 b/packages/frontend-shared/themes/d-u0.json5
index 32ac9ec5cf..fb707c74c3 100644
--- a/packages/frontend-shared/themes/d-u0.json5
+++ b/packages/frontend-shared/themes/d-u0.json5
@@ -55,7 +55,7 @@
 		codeBoolean: '#c59eff',
 		dateLabelFg: '@fg',
 		inputBorder: 'rgba(255, 255, 255, 0.1)',
-		panelBorder: '" solid 1px var(--divider)',
+		panelBorder: '" solid 1px var(--MI_THEME-divider)',
 		accentDarken: ':darken<10<@accent',
 		acrylicPanel: ':alpha<0.5<@panel',
 		navIndicator: '@indicator',
@@ -69,7 +69,6 @@
 		buttonGradateB: ':hue<20<@accent',
 		htmlThemeColor: '@bg',
 		panelHighlight: ':lighten<3<@panel',
-		listItemHoverBg: 'rgba(255, 255, 255, 0.03)',
 		scrollbarHandle: 'rgba(255, 255, 255, 0.2)',
 		inputBorderHover: 'rgba(255, 255, 255, 0.2)',
 		wallpaperOverlay: 'rgba(0, 0, 0, 0.5)',
diff --git a/packages/frontend-shared/themes/l-u0.json5 b/packages/frontend-shared/themes/l-u0.json5
index 0b952b003a..7062e7fe5b 100644
--- a/packages/frontend-shared/themes/l-u0.json5
+++ b/packages/frontend-shared/themes/l-u0.json5
@@ -56,7 +56,7 @@
 		codeBoolean: '#c59eff',
 		dateLabelFg: '@fg',
 		inputBorder: 'rgba(255, 255, 255, 0.1)',
-		panelBorder: '" solid 1px var(--divider)',
+		panelBorder: '" solid 1px var(--MI_THEME-divider)',
 		accentDarken: ':darken<10<@accent',
 		acrylicPanel: ':alpha<0.5<@panel',
 		navIndicator: '@indicator',
@@ -71,7 +71,6 @@
 		buttonGradateB: ':hue<20<@accent',
 		htmlThemeColor: '@bg',
 		panelHighlight: ':lighten<3<@panel',
-		listItemHoverBg: 'rgba(255, 255, 255, 0.03)',
 		scrollbarHandle: '#74747433',
 		inputBorderHover: 'rgba(255, 255, 255, 0.2)',
 		wallpaperOverlay: 'rgba(0, 0, 0, 0.5)',
diff --git a/packages/frontend-shared/themes/l-vivid.json5 b/packages/frontend-shared/themes/l-vivid.json5
index f1c63dde6e..39768d4ac6 100644
--- a/packages/frontend-shared/themes/l-vivid.json5
+++ b/packages/frontend-shared/themes/l-vivid.json5
@@ -39,7 +39,7 @@
 		dateLabelFg: '@fg',
 		inputBorder: 'rgba(0, 0, 0, 0.1)',
 		inputBorderHover: 'rgba(0, 0, 0, 0.2)',
-		panelBorder: '" solid 1px var(--divider)',
+		panelBorder: '" solid 1px var(--MI_THEME-divider)',
 		accentDarken: ':darken<10<@accent',
 		acrylicPanel: ':alpha<0.5<@panel',
 		navIndicator: '@accent',
@@ -52,7 +52,6 @@
 		panelHeaderFg: '@fg',
 		htmlThemeColor: '@bg',
 		panelHighlight: ':darken<3<@panel',
-		listItemHoverBg: 'rgba(0, 0, 0, 0.03)',
 		scrollbarHandle: 'rgba(0, 0, 0, 0.2)',
 		wallpaperOverlay: 'rgba(255, 255, 255, 0.5)',
 		fgTransparentWeak: ':alpha<0.75<@fg',
diff --git a/packages/frontend/src/_dev_boot_.ts b/packages/frontend/src/_dev_boot_.ts
index 1601f247d7..f312765dcf 100644
--- a/packages/frontend/src/_dev_boot_.ts
+++ b/packages/frontend/src/_dev_boot_.ts
@@ -43,7 +43,7 @@ async function main() {
 	const theme = localStorage.getItem('theme');
 	if (theme) {
 		for (const [k, v] of Object.entries(JSON.parse(theme))) {
-			document.documentElement.style.setProperty(`--${k}`, v.toString());
+			document.documentElement.style.setProperty(`--MI_THEME-${k}`, v.toString());
 
 			// HTMLの theme-color 適用
 			if (k === 'htmlThemeColor') {
diff --git a/packages/frontend/src/boot/common.ts b/packages/frontend/src/boot/common.ts
index af05f657c8..52f8fb49e5 100644
--- a/packages/frontend/src/boot/common.ts
+++ b/packages/frontend/src/boot/common.ts
@@ -182,12 +182,6 @@ export async function common(createVue: () => App<Element>) {
 			if (instance.defaultLightTheme != null) ColdDeviceStorage.set('lightTheme', JSON.parse(instance.defaultLightTheme));
 			if (instance.defaultDarkTheme != null) ColdDeviceStorage.set('darkTheme', JSON.parse(instance.defaultDarkTheme));
 			defaultStore.set('themeInitial', false);
-		} else {
-			if (defaultStore.state.darkMode) {
-				applyTheme(darkTheme.value);
-			} else {
-				applyTheme(lightTheme.value);
-			}
 		}
 	});
 
diff --git a/packages/frontend/src/components/MkAbuseReport.vue b/packages/frontend/src/components/MkAbuseReport.vue
index 0278cb30f0..b9413270ae 100644
--- a/packages/frontend/src/components/MkAbuseReport.vue
+++ b/packages/frontend/src/components/MkAbuseReport.vue
@@ -6,10 +6,10 @@ SPDX-License-Identifier: AGPL-3.0-only
 <template>
 <MkFolder>
 	<template #icon>
-		<i v-if="report.resolved && report.resolvedAs === 'accept'" class="ti ti-check" style="color: var(--success)"></i>
-		<i v-else-if="report.resolved && report.resolvedAs === 'reject'" class="ti ti-x" style="color: var(--error)"></i>
+		<i v-if="report.resolved && report.resolvedAs === 'accept'" class="ti ti-check" style="color: var(--MI_THEME-success)"></i>
+		<i v-else-if="report.resolved && report.resolvedAs === 'reject'" class="ti ti-x" style="color: var(--MI_THEME-error)"></i>
 		<i v-else-if="report.resolved" class="ti ti-slash"></i>
-		<i v-else class="ti ti-exclamation-circle" style="color: var(--warn)"></i>
+		<i v-else class="ti ti-exclamation-circle" style="color: var(--MI_THEME-warn)"></i>
 	</template>
 	<template #label><MkAcct :user="report.targetUser"/> (by <MkAcct :user="report.reporter"/>)</template>
 	<template #caption>{{ report.comment }}</template>
@@ -17,8 +17,8 @@ SPDX-License-Identifier: AGPL-3.0-only
 	<template #footer>
 		<div class="_buttons">
 			<template v-if="!report.resolved">
-				<MkButton @click="resolve('accept')"><i class="ti ti-check" style="color: var(--success)"></i> {{ i18n.ts._abuseUserReport.resolve }} ({{ i18n.ts._abuseUserReport.accept }})</MkButton>
-				<MkButton @click="resolve('reject')"><i class="ti ti-x" style="color: var(--error)"></i> {{ i18n.ts._abuseUserReport.resolve }} ({{ i18n.ts._abuseUserReport.reject }})</MkButton>
+				<MkButton @click="resolve('accept')"><i class="ti ti-check" style="color: var(--MI_THEME-success)"></i> {{ i18n.ts._abuseUserReport.resolve }} ({{ i18n.ts._abuseUserReport.accept }})</MkButton>
+				<MkButton @click="resolve('reject')"><i class="ti ti-x" style="color: var(--MI_THEME-error)"></i> {{ i18n.ts._abuseUserReport.resolve }} ({{ i18n.ts._abuseUserReport.reject }})</MkButton>
 				<MkButton @click="resolve(null)"><i class="ti ti-slash"></i> {{ i18n.ts._abuseUserReport.resolve }} ({{ i18n.ts.other }})</MkButton>
 			</template>
 			<template v-if="report.targetUser.host != null">
diff --git a/packages/frontend/src/components/MkAccountMoved.vue b/packages/frontend/src/components/MkAccountMoved.vue
index 796524fce9..bd6f8ceb09 100644
--- a/packages/frontend/src/components/MkAccountMoved.vue
+++ b/packages/frontend/src/components/MkAccountMoved.vue
@@ -32,8 +32,8 @@ misskeyApi('users/show', { userId: props.movedTo }).then(u => user.value = u);
 .root {
 	padding: 16px;
 	font-size: 90%;
-	background: var(--infoWarnBg);
-	color: var(--error);
+	background: var(--MI_THEME-infoWarnBg);
+	color: var(--MI_THEME-error);
 	border-radius: var(--radius);
 }
 
diff --git a/packages/frontend/src/components/MkAnalogClock.vue b/packages/frontend/src/components/MkAnalogClock.vue
index 835efbd6cd..c8fa6246e0 100644
--- a/packages/frontend/src/components/MkAnalogClock.vue
+++ b/packages/frontend/src/components/MkAnalogClock.vue
@@ -193,12 +193,12 @@ tick();
 
 function calcColors() {
 	const computedStyle = getComputedStyle(document.documentElement);
-	const dark = tinycolor(computedStyle.getPropertyValue('--bg')).isDark();
-	const accent = tinycolor(computedStyle.getPropertyValue('--accent')).toHexString();
+	const dark = tinycolor(computedStyle.getPropertyValue('--MI_THEME-bg')).isDark();
+	const accent = tinycolor(computedStyle.getPropertyValue('--MI_THEME-accent')).toHexString();
 	majorGraduationColor.value = dark ? 'rgba(255, 255, 255, 0.3)' : 'rgba(0, 0, 0, 0.3)';
 	//minorGraduationColor = dark ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
 	sHandColor.value = dark ? 'rgba(255, 255, 255, 0.5)' : 'rgba(0, 0, 0, 0.3)';
-	mHandColor.value = tinycolor(computedStyle.getPropertyValue('--fg')).toHexString();
+	mHandColor.value = tinycolor(computedStyle.getPropertyValue('--MI_THEME-fg')).toHexString();
 	hHandColor.value = accent;
 	nowColor.value = accent;
 }
diff --git a/packages/frontend/src/components/MkAnnouncementDialog.vue b/packages/frontend/src/components/MkAnnouncementDialog.vue
index f27694658e..488492701e 100644
--- a/packages/frontend/src/components/MkAnnouncementDialog.vue
+++ b/packages/frontend/src/components/MkAnnouncementDialog.vue
@@ -9,9 +9,9 @@ SPDX-License-Identifier: AGPL-3.0-only
 		<div :class="$style.header">
 			<span :class="$style.icon">
 				<i v-if="announcement.icon === 'info'" class="ti ti-info-circle"></i>
-				<i v-else-if="announcement.icon === 'warning'" class="ti ti-alert-triangle" style="color: var(--warn);"></i>
-				<i v-else-if="announcement.icon === 'error'" class="ti ti-circle-x" style="color: var(--error);"></i>
-				<i v-else-if="announcement.icon === 'success'" class="ti ti-check" style="color: var(--success);"></i>
+				<i v-else-if="announcement.icon === 'warning'" class="ti ti-alert-triangle" style="color: var(--MI_THEME-warn);"></i>
+				<i v-else-if="announcement.icon === 'error'" class="ti ti-circle-x" style="color: var(--MI_THEME-error);"></i>
+				<i v-else-if="announcement.icon === 'success'" class="ti ti-check" style="color: var(--MI_THEME-success);"></i>
 			</span>
 			<span :class="$style.title">{{ announcement.title }}</span>
 		</div>
@@ -83,7 +83,7 @@ onMounted(() => {
 	min-width: 320px;
 	max-width: 480px;
 	box-sizing: border-box;
-	background: var(--panel);
+	background: var(--MI_THEME-panel);
 	border-radius: var(--radius);
 }
 
diff --git a/packages/frontend/src/components/MkAntennaEditor.vue b/packages/frontend/src/components/MkAntennaEditor.vue
index cb7ee3d6ca..2386ba6fa7 100644
--- a/packages/frontend/src/components/MkAntennaEditor.vue
+++ b/packages/frontend/src/components/MkAntennaEditor.vue
@@ -170,6 +170,6 @@ function addUser() {
 .actions {
 	margin-top: 16px;
 	padding: 24px 0;
-	border-top: solid 0.5px var(--divider);
+	border-top: solid 0.5px var(--MI_THEME-divider);
 }
 </style>
diff --git a/packages/frontend/src/components/MkAsUi.vue b/packages/frontend/src/components/MkAsUi.vue
index b50a7fea5c..e52ab5ccad 100644
--- a/packages/frontend/src/components/MkAsUi.vue
+++ b/packages/frontend/src/components/MkAsUi.vue
@@ -106,7 +106,7 @@ const containerStyle = computed(() => {
 
 	const border = isBordered ? {
 		borderWidth: c.borderWidth ?? '1px',
-		borderColor: c.borderColor ?? 'var(--divider)',
+		borderColor: c.borderColor ?? 'var(--MI_THEME-divider)',
 		borderStyle: c.borderStyle ?? 'solid',
 	} : undefined;
 
@@ -165,7 +165,7 @@ function openPostForm() {
 }
 
 .postForm {
-	background: var(--bg);
+	background: var(--MI_THEME-bg);
 	border-radius: 8px;
 }
 </style>
diff --git a/packages/frontend/src/components/MkAutocomplete.vue b/packages/frontend/src/components/MkAutocomplete.vue
index f547991369..0ea4566d4e 100644
--- a/packages/frontend/src/components/MkAutocomplete.vue
+++ b/packages/frontend/src/components/MkAutocomplete.vue
@@ -407,16 +407,16 @@ onBeforeUnmount(() => {
 	text-overflow: ellipsis;
 
 	&:hover {
-		background: var(--X3);
+		background: var(--MI_THEME-X3);
 	}
 
 	&[data-selected='true'] {
-		background: var(--accent);
+		background: var(--MI_THEME-accent);
 		color: #fff !important;
 	}
 
 	&:active {
-		background: var(--accentDarken);
+		background: var(--MI_THEME-accentDarken);
 		color: #fff !important;
 	}
 }
diff --git a/packages/frontend/src/components/MkButton.vue b/packages/frontend/src/components/MkButton.vue
index 1156b3f2b8..311facb4aa 100644
--- a/packages/frontend/src/components/MkButton.vue
+++ b/packages/frontend/src/components/MkButton.vue
@@ -129,7 +129,7 @@ function onMousedown(evt: MouseEvent): void {
 	font-size: 95%;
 	box-shadow: none;
 	text-decoration: none;
-	background: var(--buttonBg);
+	background: var(--MI_THEME-buttonBg);
 	border-radius: 5px;
 	overflow: clip;
 	box-sizing: border-box;
@@ -140,11 +140,11 @@ function onMousedown(evt: MouseEvent): void {
 	}
 
 	&:not(:disabled):hover {
-		background: var(--buttonHoverBg);
+		background: var(--MI_THEME-buttonHoverBg);
 	}
 
 	&:not(:disabled):active {
-		background: var(--buttonHoverBg);
+		background: var(--MI_THEME-buttonHoverBg);
 	}
 
 	&.small {
@@ -167,15 +167,15 @@ function onMousedown(evt: MouseEvent): void {
 
 	&.primary {
 		font-weight: bold;
-		color: var(--fgOnAccent) !important;
-		background: var(--accent);
+		color: var(--MI_THEME-fgOnAccent) !important;
+		background: var(--MI_THEME-accent);
 
 		&:not(:disabled):hover {
-			background: hsl(from var(--accent) h s calc(l + 5));
+			background: hsl(from var(--MI_THEME-accent) h s calc(l + 5));
 		}
 
 		&:not(:disabled):active {
-			background: hsl(from var(--accent) h s calc(l + 5));
+			background: hsl(from var(--MI_THEME-accent) h s calc(l + 5));
 		}
 	}
 
@@ -216,15 +216,15 @@ function onMousedown(evt: MouseEvent): void {
 
 	&.gradate {
 		font-weight: bold;
-		color: var(--fgOnAccent) !important;
-		background: linear-gradient(90deg, var(--buttonGradateA), var(--buttonGradateB));
+		color: var(--MI_THEME-fgOnAccent) !important;
+		background: linear-gradient(90deg, var(--MI_THEME-buttonGradateA), var(--MI_THEME-buttonGradateB));
 
 		&:not(:disabled):hover {
-			background: linear-gradient(90deg, hsl(from var(--accent) h s calc(l + 5)), hsl(from var(--accent) h s calc(l + 5)));
+			background: linear-gradient(90deg, hsl(from var(--MI_THEME-accent) h s calc(l + 5)), hsl(from var(--MI_THEME-accent) h s calc(l + 5)));
 		}
 
 		&:not(:disabled):active {
-			background: linear-gradient(90deg, hsl(from var(--accent) h s calc(l + 5)), hsl(from var(--accent) h s calc(l + 5)));
+			background: linear-gradient(90deg, hsl(from var(--MI_THEME-accent) h s calc(l + 5)), hsl(from var(--MI_THEME-accent) h s calc(l + 5)));
 		}
 	}
 
diff --git a/packages/frontend/src/components/MkChannelFollowButton.vue b/packages/frontend/src/components/MkChannelFollowButton.vue
index 35dc3ad4bf..d4e4f6179a 100644
--- a/packages/frontend/src/components/MkChannelFollowButton.vue
+++ b/packages/frontend/src/components/MkChannelFollowButton.vue
@@ -68,9 +68,9 @@ async function onClick() {
 	position: relative;
 	display: inline-block;
 	font-weight: bold;
-	color: var(--accent);
+	color: var(--MI_THEME-accent);
 	background: transparent;
-	border: solid 1px var(--accent);
+	border: solid 1px var(--MI_THEME-accent);
 	padding: 0;
 	height: 31px;
 	font-size: 16px;
@@ -99,17 +99,17 @@ async function onClick() {
 	}
 
 	&.active {
-		color: var(--fgOnAccent);
-		background: var(--accent);
+		color: var(--MI_THEME-fgOnAccent);
+		background: var(--MI_THEME-accent);
 
 		&:hover {
-			background: var(--accentLighten);
-			border-color: var(--accentLighten);
+			background: var(--MI_THEME-accentLighten);
+			border-color: var(--MI_THEME-accentLighten);
 		}
 
 		&:active {
-			background: var(--accentDarken);
-			border-color: var(--accentDarken);
+			background: var(--MI_THEME-accentDarken);
+			border-color: var(--MI_THEME-accentDarken);
 		}
 	}
 
diff --git a/packages/frontend/src/components/MkChannelPreview.vue b/packages/frontend/src/components/MkChannelPreview.vue
index 3c0874a1eb..99580df5e2 100644
--- a/packages/frontend/src/components/MkChannelPreview.vue
+++ b/packages/frontend/src/components/MkChannelPreview.vue
@@ -100,7 +100,7 @@ const bannerStyle = computed(() => {
 			height: 100%;
 			border-radius: inherit;
 			pointer-events: none;
-			box-shadow: inset 0 0 0 2px var(--focus);
+			box-shadow: inset 0 0 0 2px var(--MI_THEME-focus);
 		}
 	}
 
@@ -117,7 +117,7 @@ const bannerStyle = computed(() => {
 			left: 0;
 			width: 100%;
 			height: 64px;
-			background: linear-gradient(0deg, var(--panel), color(from var(--panel) srgb r g b / 0));
+			background: linear-gradient(0deg, var(--MI_THEME-panel), color(from var(--MI_THEME-panel) srgb r g b / 0));
 		}
 
 		> .name {
@@ -148,7 +148,7 @@ const bannerStyle = computed(() => {
 			bottom: 16px;
 			left: 16px;
 			background: rgba(0, 0, 0, 0.7);
-			color: var(--warn);
+			color: var(--MI_THEME-warn);
 			border-radius: 6px;
 			font-weight: bold;
 			font-size: 1em;
@@ -167,7 +167,7 @@ const bannerStyle = computed(() => {
 
 	> footer {
 		padding: 12px 16px;
-		border-top: solid 0.5px var(--divider);
+		border-top: solid 0.5px var(--MI_THEME-divider);
 
 		> span {
 			opacity: 0.7;
@@ -213,8 +213,8 @@ const bannerStyle = computed(() => {
 	top: 0;
 	right: 0;
 	transform: translate(25%, -25%);
-	background-color: var(--accent);
-	border: solid var(--bg) 4px;
+	background-color: var(--MI_THEME-accent);
+	border: solid var(--MI_THEME-bg) 4px;
 	border-radius: 100%;
 	width: 1.5rem;
 	height: 1.5rem;
diff --git a/packages/frontend/src/components/MkChartLegend.vue b/packages/frontend/src/components/MkChartLegend.vue
index 6eb2009784..574cde9da4 100644
--- a/packages/frontend/src/components/MkChartLegend.vue
+++ b/packages/frontend/src/components/MkChartLegend.vue
@@ -53,11 +53,11 @@ defineExpose({
 		> .item {
 			font-size: 85%;
 			padding: 4px 12px 4px 8px;
-			border: solid 1px var(--divider);
+			border: solid 1px var(--MI_THEME-divider);
 			border-radius: 999px;
 
 			&:hover {
-				border-color: var(--inputBorderHover);
+				border-color: var(--MI_THEME-inputBorderHover);
 			}
 
 			&.disabled {
diff --git a/packages/frontend/src/components/MkClipPreview.vue b/packages/frontend/src/components/MkClipPreview.vue
index dd550733cb..5b09ec90dd 100644
--- a/packages/frontend/src/components/MkClipPreview.vue
+++ b/packages/frontend/src/components/MkClipPreview.vue
@@ -49,13 +49,13 @@ const remaining = computed(() => {
 		outline: none;
 
 		.root {
-			box-shadow: inset 0 0 0 2px var(--focus);
+			box-shadow: inset 0 0 0 2px var(--MI_THEME-focus);
 		}
 	}
 
 	&:hover {
 		text-decoration: none;
-		color: var(--accent);
+		color: var(--MI_THEME-accent);
 	}
 }
 
@@ -65,7 +65,7 @@ const remaining = computed(() => {
 
 .divider {
 	height: 1px;
-	background: var(--divider);
+	background: var(--MI_THEME-divider);
 }
 
 .description {
diff --git a/packages/frontend/src/components/MkCode.core.vue b/packages/frontend/src/components/MkCode.core.vue
index c0e7df5dac..0d7a67eaec 100644
--- a/packages/frontend/src/components/MkCode.core.vue
+++ b/packages/frontend/src/components/MkCode.core.vue
@@ -77,7 +77,7 @@ watch(() => props.lang, (to) => {
 	margin: .5em 0;
 	overflow: auto;
 	border-radius: 8px;
-	border: 1px solid var(--divider);
+	border: 1px solid var(--MI_THEME-divider);
 	font-family: Consolas, Monaco, Andale Mono, Ubuntu Mono, monospace;
 
 	color: var(--shiki-fallback);
diff --git a/packages/frontend/src/components/MkCode.vue b/packages/frontend/src/components/MkCode.vue
index 716dd92678..cb82bfd98b 100644
--- a/packages/frontend/src/components/MkCode.vue
+++ b/packages/frontend/src/components/MkCode.vue
@@ -71,7 +71,7 @@ function copy() {
 .codeBlockFallbackRoot {
 	display: block;
 	overflow-wrap: anywhere;
-	background: var(--bg);
+	background: var(--MI_THEME-bg);
 	padding: 1em;
 	margin: .5em 0;
 	overflow: auto;
@@ -94,8 +94,8 @@ function copy() {
 	border-radius: 8px;
 	padding: 24px;
 	margin-top: 4px;
-	color: var(--fg);
-	background: var(--bg);
+	color: var(--MI_THEME-fg);
+	background: var(--MI_THEME-bg);
 }
 
 .codePlaceholderContainer {
diff --git a/packages/frontend/src/components/MkCodeEditor.vue b/packages/frontend/src/components/MkCodeEditor.vue
index afd9132a12..5bf2301e72 100644
--- a/packages/frontend/src/components/MkCodeEditor.vue
+++ b/packages/frontend/src/components/MkCodeEditor.vue
@@ -140,7 +140,7 @@ watch(v, newValue => {
 .caption {
 	font-size: 0.85em;
 	padding: 8px 0 0 0;
-	color: var(--fgTransparentWeak);
+	color: var(--MI_THEME-fgTransparentWeak);
 
 	&:empty {
 		display: none;
@@ -160,17 +160,17 @@ watch(v, newValue => {
 	margin: 0;
 	border-radius: 6px;
 	padding: 0;
-	color: var(--fg);
-	border: solid 1px var(--panel);
+	color: var(--MI_THEME-fg);
+	border: solid 1px var(--MI_THEME-panel);
 	transition: border-color 0.1s ease-out;
 	font-family: Consolas, Monaco, Andale Mono, Ubuntu Mono, monospace;
 	&:hover {
-		border-color: var(--inputBorderHover) !important;
+		border-color: var(--MI_THEME-inputBorderHover) !important;
 	}
 }
 
 .focused.codeEditorRoot {
-	border-color: var(--accent) !important;
+	border-color: var(--MI_THEME-accent) !important;
 	border-radius: 6px;
 }
 
@@ -196,7 +196,7 @@ watch(v, newValue => {
 	resize: none;
 	text-align: left;
 	color: transparent;
-	caret-color: var(--fg);
+	caret-color: var(--MI_THEME-fg);
 	background-color: transparent;
 	border: 0;
 	border-radius: 6px;
@@ -211,6 +211,6 @@ watch(v, newValue => {
 }
 
 .textarea::selection {
-	color: var(--bg);
+	color: var(--MI_THEME-bg);
 }
 </style>
diff --git a/packages/frontend/src/components/MkCodeInline.vue b/packages/frontend/src/components/MkCodeInline.vue
index 6add80d1bc..04b6e54108 100644
--- a/packages/frontend/src/components/MkCodeInline.vue
+++ b/packages/frontend/src/components/MkCodeInline.vue
@@ -18,7 +18,7 @@ const props = defineProps<{
 	display: inline-block;
 	font-family: Consolas, Monaco, Andale Mono, Ubuntu Mono, monospace;
 	overflow-wrap: anywhere;
-	background: var(--bg);
+	background: var(--MI_THEME-bg);
 	padding: .1em;
 	border-radius: .3em;
 }
diff --git a/packages/frontend/src/components/MkColorInput.vue b/packages/frontend/src/components/MkColorInput.vue
index f5c580789b..55a32664de 100644
--- a/packages/frontend/src/components/MkColorInput.vue
+++ b/packages/frontend/src/components/MkColorInput.vue
@@ -60,7 +60,7 @@ const onInput = () => {
 .caption {
 	font-size: 0.85em;
 	padding: 8px 0 0 0;
-	color: var(--fgTransparentWeak);
+	color: var(--MI_THEME-fgTransparentWeak);
 
 	&:empty {
 		display: none;
@@ -72,8 +72,8 @@ const onInput = () => {
 
 	&.focused {
 		> .inputCore {
-			border-color: var(--accent) !important;
-			//box-shadow: 0 0 0 4px var(--focus);
+			border-color: var(--MI_THEME-accent) !important;
+			//box-shadow: 0 0 0 4px var(--MI_THEME-focus);
 		}
 	}
 
@@ -98,9 +98,9 @@ const onInput = () => {
 	font: inherit;
 	font-weight: normal;
 	font-size: 1em;
-	color: var(--fg);
-	background: var(--panel);
-	border: solid 1px var(--panel);
+	color: var(--MI_THEME-fg);
+	background: var(--MI_THEME-panel);
+	border: solid 1px var(--MI_THEME-panel);
 	border-radius: 6px;
 	outline: none;
 	box-shadow: none;
@@ -108,7 +108,7 @@ const onInput = () => {
 	transition: border-color 0.1s ease-out;
 
 	&:hover {
-		border-color: var(--inputBorderHover) !important;
+		border-color: var(--MI_THEME-inputBorderHover) !important;
 	}
 }
 </style>
diff --git a/packages/frontend/src/components/MkContainer.vue b/packages/frontend/src/components/MkContainer.vue
index 8ad653a0bf..f2bafb4adf 100644
--- a/packages/frontend/src/components/MkContainer.vue
+++ b/packages/frontend/src/components/MkContainer.vue
@@ -167,9 +167,9 @@ onUnmounted(() => {
 	position: sticky;
 	top: var(--stickyTop, 0px);
 	left: 0;
-	color: var(--panelHeaderFg);
-	background: var(--panelHeaderBg);
-	border-bottom: solid 0.5px var(--panelHeaderDivider);
+	color: var(--MI_THEME-panelHeaderFg);
+	background: var(--MI_THEME-panelHeaderBg);
+	border-bottom: solid 0.5px var(--MI_THEME-panelHeaderDivider);
 	z-index: 2;
 	line-height: 1.4em;
 }
@@ -216,11 +216,11 @@ onUnmounted(() => {
 			left: 0;
 			width: 100%;
 			height: 64px;
-			background: linear-gradient(0deg, var(--panel), color(from var(--panel) srgb r g b / 0));
+			background: linear-gradient(0deg, var(--MI_THEME-panel), color(from var(--MI_THEME-panel) srgb r g b / 0));
 
 			> .fadeLabel {
 				display: inline-block;
-				background: var(--panel);
+				background: var(--MI_THEME-panel);
 				padding: 6px 10px;
 				font-size: 0.8em;
 				border-radius: 999px;
@@ -229,7 +229,7 @@ onUnmounted(() => {
 
 			&:hover {
 				> .fadeLabel {
-					background: var(--panelHighlight);
+					background: var(--MI_THEME-panelHighlight);
 				}
 			}
 		}
diff --git a/packages/frontend/src/components/MkCropperDialog.vue b/packages/frontend/src/components/MkCropperDialog.vue
index 2e1e92cbdf..a25dc36882 100644
--- a/packages/frontend/src/components/MkCropperDialog.vue
+++ b/packages/frontend/src/components/MkCropperDialog.vue
@@ -125,7 +125,7 @@ onMounted(() => {
 	const computedStyle = getComputedStyle(document.documentElement);
 
 	const selection = cropper.getCropperSelection()!;
-	selection.themeColor = tinycolor(computedStyle.getPropertyValue('--accent')).toHexString();
+	selection.themeColor = tinycolor(computedStyle.getPropertyValue('--MI_THEME-accent')).toHexString();
 	selection.aspectRatio = props.aspectRatio;
 	selection.initialAspectRatio = props.aspectRatio;
 	selection.outlined = true;
diff --git a/packages/frontend/src/components/MkCustomEmojiDetailedDialog.vue b/packages/frontend/src/components/MkCustomEmojiDetailedDialog.vue
index c7f1288729..29a435fb1a 100644
--- a/packages/frontend/src/components/MkCustomEmojiDetailedDialog.vue
+++ b/packages/frontend/src/components/MkCustomEmojiDetailedDialog.vue
@@ -85,7 +85,7 @@ function cancel() {
 .emojiImgWrapper {
   max-width: 100%;
   height: 40cqh;
-  background-image: repeating-linear-gradient(45deg, transparent, transparent 8px, var(--X5) 8px, var(--X5) 14px);
+  background-image: repeating-linear-gradient(45deg, transparent, transparent 8px, var(--MI_THEME-X5) 8px, var(--MI_THEME-X5) 14px);
   border-radius: var(--radius);
   margin: auto;
   overflow-y: hidden;
@@ -101,8 +101,8 @@ function cancel() {
   display: inline-block;
   word-break: break-all;
   padding: 3px 10px;
-  background-color: var(--X5);
-  border: solid 1px var(--divider);
+  background-color: var(--MI_THEME-X5);
+  border: solid 1px var(--MI_THEME-divider);
   border-radius: var(--radius);
 }
 </style>
diff --git a/packages/frontend/src/components/MkDateSeparatedList.vue b/packages/frontend/src/components/MkDateSeparatedList.vue
index 4b94bef4b6..0886b7a4f7 100644
--- a/packages/frontend/src/components/MkDateSeparatedList.vue
+++ b/packages/frontend/src/components/MkDateSeparatedList.vue
@@ -194,7 +194,7 @@ export default defineComponent({
 		box-shadow: none;
 
 		&:not(:last-child) {
-			border-bottom: solid 0.5px var(--divider);
+			border-bottom: solid 0.5px var(--MI_THEME-divider);
 		}
 	}
 }
@@ -235,7 +235,7 @@ export default defineComponent({
 	line-height: 32px;
 	text-align: center;
 	font-size: 12px;
-	color: var(--dateLabelFg);
+	color: var(--MI_THEME-dateLabelFg);
 }
 
 .date-1 {
diff --git a/packages/frontend/src/components/MkDialog.vue b/packages/frontend/src/components/MkDialog.vue
index 16cf5b1b75..22130d4fab 100644
--- a/packages/frontend/src/components/MkDialog.vue
+++ b/packages/frontend/src/components/MkDialog.vue
@@ -184,7 +184,7 @@ function onInputKeydown(evt: KeyboardEvent) {
 	max-width: 480px;
 	box-sizing: border-box;
 	text-align: center;
-	background: var(--panel);
+	background: var(--MI_THEME-panel);
 	border-radius: 16px;
 }
 
@@ -206,15 +206,15 @@ function onInputKeydown(evt: KeyboardEvent) {
 }
 
 .type_success {
-	color: var(--success);
+	color: var(--MI_THEME-success);
 }
 
 .type_error {
-	color: var(--error);
+	color: var(--MI_THEME-error);
 }
 
 .type_warning {
-	color: var(--warn);
+	color: var(--MI_THEME-warn);
 }
 
 .title {
diff --git a/packages/frontend/src/components/MkDivider.vue b/packages/frontend/src/components/MkDivider.vue
index e4e3af99e4..f72f091383 100644
--- a/packages/frontend/src/components/MkDivider.vue
+++ b/packages/frontend/src/components/MkDivider.vue
@@ -27,6 +27,6 @@ defineProps<{
 
 <style scoped lang="scss">
 .default {
-	border-top: solid 0.5px var(--divider);
+	border-top: solid 0.5px var(--MI_THEME-divider);
 }
 </style>
diff --git a/packages/frontend/src/components/MkDonation.vue b/packages/frontend/src/components/MkDonation.vue
index 098be07a8c..ebface5185 100644
--- a/packages/frontend/src/components/MkDonation.vue
+++ b/packages/frontend/src/components/MkDonation.vue
@@ -79,7 +79,7 @@ function neverShow() {
 	text-align: center;
 	padding-top: 25px;
 	width: 100px;
-	color: var(--accent);
+	color: var(--MI_THEME-accent);
 }
 @media (max-width: 500px) {
 	.icon {
diff --git a/packages/frontend/src/components/MkDrive.file.vue b/packages/frontend/src/components/MkDrive.file.vue
index 90284890a5..e45c3bd9ce 100644
--- a/packages/frontend/src/components/MkDrive.file.vue
+++ b/packages/frontend/src/components/MkDrive.file.vue
@@ -148,14 +148,14 @@ function onDragend() {
 	}
 
 	&.isSelected {
-		background: var(--accent);
+		background: var(--MI_THEME-accent);
 
 		&:hover {
-			background: var(--accentLighten);
+			background: var(--MI_THEME-accentLighten);
 		}
 
 		&:active {
-			background: var(--accentDarken);
+			background: var(--MI_THEME-accentDarken);
 		}
 
 		> .label {
@@ -244,7 +244,7 @@ function onDragend() {
 	font-size: 0.8em;
 	text-align: center;
 	word-break: break-all;
-	color: var(--fg);
+	color: var(--MI_THEME-fg);
 	overflow: hidden;
 }
 </style>
diff --git a/packages/frontend/src/components/MkDrive.folder.vue b/packages/frontend/src/components/MkDrive.folder.vue
index 92b3a23662..391acbc8d3 100644
--- a/packages/frontend/src/components/MkDrive.folder.vue
+++ b/packages/frontend/src/components/MkDrive.folder.vue
@@ -313,7 +313,7 @@ function onContextmenu(ev: MouseEvent) {
 	position: relative;
 	padding: 8px;
 	height: 64px;
-	background: var(--driveFolderBg);
+	background: var(--MI_THEME-driveFolderBg);
 	border-radius: 4px;
 	cursor: pointer;
 
@@ -326,7 +326,7 @@ function onContextmenu(ev: MouseEvent) {
 			right: -4px;
 			bottom: -4px;
 			left: -4px;
-			border: 2px dashed var(--focus);
+			border: 2px dashed var(--MI_THEME-focus);
 			border-radius: 4px;
 		}
 	}
@@ -345,13 +345,13 @@ function onContextmenu(ev: MouseEvent) {
 		width: 18px;
 		height: 18px;
 		background: #fff;
-		border: solid 2px var(--divider);
+		border: solid 2px var(--MI_THEME-divider);
 		border-radius: 4px;
 		box-sizing: border-box;
 
 		&.checked {
-			border-color: var(--accent);
-			background: var(--accent);
+			border-color: var(--MI_THEME-accent);
+			background: var(--MI_THEME-accent);
 
 			&::after {
 				content: "\ea5e";
@@ -368,7 +368,7 @@ function onContextmenu(ev: MouseEvent) {
 	}
 
 	&:hover {
-		background: var(--accentedBg);
+		background: var(--MI_THEME-accentedBg);
 	}
 }
 
diff --git a/packages/frontend/src/components/MkDrive.vue b/packages/frontend/src/components/MkDrive.vue
index d9ca0a72a0..8bd7ee8324 100644
--- a/packages/frontend/src/components/MkDrive.vue
+++ b/packages/frontend/src/components/MkDrive.vue
@@ -721,7 +721,7 @@ onBeforeUnmount(() => {
 	box-sizing: border-box;
 	overflow: auto;
 	font-size: 0.9em;
-	box-shadow: 0 1px 0 var(--divider);
+	box-shadow: 0 1px 0 var(--MI_THEME-divider);
 	user-select: none;
 }
 
@@ -815,7 +815,7 @@ onBeforeUnmount(() => {
 	top: 38px;
 	width: 100%;
 	height: calc(100% - 38px);
-	border: dashed 2px var(--focus);
+	border: dashed 2px var(--MI_THEME-focus);
 	pointer-events: none;
 }
 </style>
diff --git a/packages/frontend/src/components/MkDriveFileThumbnail.vue b/packages/frontend/src/components/MkDriveFileThumbnail.vue
index eb93aaab6e..3410a915c3 100644
--- a/packages/frontend/src/components/MkDriveFileThumbnail.vue
+++ b/packages/frontend/src/components/MkDriveFileThumbnail.vue
@@ -69,7 +69,7 @@ const isThumbnailAvailable = computed(() => {
 .root {
 	position: relative;
 	display: flex;
-	background: var(--panel);
+	background: var(--MI_THEME-panel);
 	border-radius: 8px;
 	overflow: clip;
 }
@@ -83,7 +83,7 @@ const isThumbnailAvailable = computed(() => {
 	height: 100%;
 	pointer-events: none;
 	border-radius: inherit;
-	box-shadow: inset 0 0 0 4px var(--warn);
+	box-shadow: inset 0 0 0 4px var(--MI_THEME-warn);
 }
 
 .iconSub {
diff --git a/packages/frontend/src/components/MkEmbedCodeGenDialog.vue b/packages/frontend/src/components/MkEmbedCodeGenDialog.vue
index c060c3a659..c2bb516c7c 100644
--- a/packages/frontend/src/components/MkEmbedCodeGenDialog.vue
+++ b/packages/frontend/src/components/MkEmbedCodeGenDialog.vue
@@ -306,9 +306,9 @@ onUnmounted(() => {
 
 .embedCodeGenPreviewRoot {
 	position: relative;
-	background-color: var(--bg);
+	background-color: var(--MI_THEME-bg);
 	background-size: auto auto;
-	background-image: repeating-linear-gradient(135deg, transparent, transparent 6px, var(--panel) 6px, var(--panel) 12px);
+	background-image: repeating-linear-gradient(135deg, transparent, transparent 6px, var(--MI_THEME-panel) 6px, var(--MI_THEME-panel) 12px);
 	cursor: not-allowed;
 }
 
@@ -381,8 +381,8 @@ onUnmounted(() => {
 
 .embedCodeGenResultHeadingIcon {
 	margin: 0 auto;
-	background-color: var(--accentedBg);
-	color: var(--accent);
+	background-color: var(--MI_THEME-accentedBg);
+	color: var(--MI_THEME-accent);
 	text-align: center;
 	height: 64px;
 	width: 64px;
diff --git a/packages/frontend/src/components/MkEmojiPicker.section.vue b/packages/frontend/src/components/MkEmojiPicker.section.vue
index fca7aa2f4e..f4caa730bf 100644
--- a/packages/frontend/src/components/MkEmojiPicker.section.vue
+++ b/packages/frontend/src/components/MkEmojiPicker.section.vue
@@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 <template>
 <!-- このコンポーネントの要素のclassは親から利用されるのでむやみに弄らないこと -->
 <!-- フォルダの中にはカスタム絵文字だけ(Unicode絵文字もこっち) -->
-<section v-if="!hasChildSection" v-panel style="border-radius: 6px; border-bottom: 0.5px solid var(--divider);">
+<section v-if="!hasChildSection" v-panel style="border-radius: 6px; border-bottom: 0.5px solid var(--MI_THEME-divider);">
 	<header class="_acrylic" @click="shown = !shown">
 		<i class="toggle ti-fw" :class="shown ? 'ti ti-chevron-down' : 'ti ti-chevron-up'"></i> <slot></slot> (<i class="ti ti-icons"></i>:{{ emojis.length }})
 	</header>
@@ -26,7 +26,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 	</div>
 </section>
 <!-- フォルダの中にはカスタム絵文字やフォルダがある -->
-<section v-else v-panel style="border-radius: 6px; border-bottom: 0.5px solid var(--divider);">
+<section v-else v-panel style="border-radius: 6px; border-bottom: 0.5px solid var(--MI_THEME-divider);">
 	<header class="_acrylic" @click="shown = !shown">
 		<i class="toggle ti-fw" :class="shown ? 'ti ti-chevron-down' : 'ti ti-chevron-up'"></i> <slot></slot> (<i class="ti ti-folder ti-fw"></i>:{{ customEmojiTree?.length }} <i class="ti ti-icons ti-fw"></i>:{{ emojis.length }})
 	</header>
diff --git a/packages/frontend/src/components/MkEmojiPicker.vue b/packages/frontend/src/components/MkEmojiPicker.vue
index 3bad8da06f..219950f135 100644
--- a/packages/frontend/src/components/MkEmojiPicker.vue
+++ b/packages/frontend/src/components/MkEmojiPicker.vue
@@ -580,7 +580,7 @@ defineExpose({
 
 						&:disabled {
 							cursor: not-allowed;
-							background: linear-gradient(-45deg, transparent 0% 48%, var(--X6) 48% 52%, transparent 52% 100%);
+							background: linear-gradient(-45deg, transparent 0% 48%, var(--MI_THEME-X6) 48% 52%, transparent 52% 100%);
 							opacity: 1;
 
 							> .emoji {
@@ -615,7 +615,7 @@ defineExpose({
 
 						&:disabled {
 							cursor: not-allowed;
-							background: linear-gradient(-45deg, transparent 0% 48%, var(--X6) 48% 52%, transparent 52% 100%);
+							background: linear-gradient(-45deg, transparent 0% 48%, var(--MI_THEME-X6) 48% 52%, transparent 52% 100%);
 							opacity: 1;
 
 							> .emoji {
@@ -638,7 +638,7 @@ defineExpose({
 		outline: none;
 		border: none;
 		background: transparent;
-		color: var(--fg);
+		color: var(--MI_THEME-fg);
 
 		&:not(:focus):not(.filled) {
 			margin-bottom: env(safe-area-inset-bottom, 0px);
@@ -647,7 +647,7 @@ defineExpose({
 		&:not(.filled) {
 			order: 1;
 			z-index: 2;
-			box-shadow: 0px -1px 0 0px var(--divider);
+			box-shadow: 0px -1px 0 0px var(--MI_THEME-divider);
 		}
 	}
 
@@ -658,11 +658,11 @@ defineExpose({
 		> .tab {
 			flex: 1;
 			height: 38px;
-			border-top: solid 0.5px var(--divider);
+			border-top: solid 0.5px var(--MI_THEME-divider);
 
 			&.active {
-				border-top: solid 1px var(--accent);
-				color: var(--accent);
+				border-top: solid 1px var(--MI_THEME-accent);
+				color: var(--MI_THEME-accent);
 			}
 		}
 	}
@@ -681,7 +681,7 @@ defineExpose({
 		> .group {
 			&:not(.index) {
 				padding: 4px 0 8px 0;
-				border-top: solid 0.5px var(--divider);
+				border-top: solid 0.5px var(--MI_THEME-divider);
 			}
 
 			> header {
@@ -708,7 +708,7 @@ defineExpose({
 				cursor: pointer;
 
 				&:hover {
-					color: var(--accent);
+					color: var(--MI_THEME-accent);
 				}
 			}
 
@@ -730,13 +730,13 @@ defineExpose({
 					}
 
 					&:active {
-						background: var(--accent);
+						background: var(--MI_THEME-accent);
 						box-shadow: inset 0 0.15em 0.3em rgba(27, 31, 35, 0.15);
 					}
 
 					&:disabled {
 						cursor: not-allowed;
-						background: linear-gradient(-45deg, transparent 0% 48%, var(--X6) 48% 52%, transparent 52% 100%);
+						background: linear-gradient(-45deg, transparent 0% 48%, var(--MI_THEME-X6) 48% 52%, transparent 52% 100%);
 						opacity: 1;
 
 						> .emoji {
@@ -757,7 +757,7 @@ defineExpose({
 			}
 
 			&.result {
-				border-bottom: solid 0.5px var(--divider);
+				border-bottom: solid 0.5px var(--MI_THEME-divider);
 
 				&:empty {
 					display: none;
diff --git a/packages/frontend/src/components/MkExtensionInstaller.vue b/packages/frontend/src/components/MkExtensionInstaller.vue
index 0f7acd69e7..ed29dade7a 100644
--- a/packages/frontend/src/components/MkExtensionInstaller.vue
+++ b/packages/frontend/src/components/MkExtensionInstaller.vue
@@ -111,7 +111,7 @@ const emits = defineEmits<{
 <style lang="scss" module>
 .extInstallerRoot {
 	border-radius: var(--radius);
-	background: var(--panel);
+	background: var(--MI_THEME-panel);
 	padding: 1.5rem;
 }
 
@@ -125,8 +125,8 @@ const emits = defineEmits<{
 	margin-left: auto;
 	margin-right: auto;
 
-	background-color: var(--accentedBg);
-	color: var(--accent);
+	background-color: var(--MI_THEME-accentedBg);
+	color: var(--MI_THEME-accent);
 }
 
 .extInstallerTitle {
diff --git a/packages/frontend/src/components/MkFileListForAdmin.vue b/packages/frontend/src/components/MkFileListForAdmin.vue
index 13295c455b..d5d32ebb28 100644
--- a/packages/frontend/src/components/MkFileListForAdmin.vue
+++ b/packages/frontend/src/components/MkFileListForAdmin.vue
@@ -66,7 +66,7 @@ const props = defineProps<{
 			align-items: center;
 
 			&:hover {
-				color: var(--accent);
+				color: var(--MI_THEME-accent);
 			}
 
 			> .thumbnail {
diff --git a/packages/frontend/src/components/MkFlashPreview.vue b/packages/frontend/src/components/MkFlashPreview.vue
index 8a2a438624..589dd1ce82 100644
--- a/packages/frontend/src/components/MkFlashPreview.vue
+++ b/packages/frontend/src/components/MkFlashPreview.vue
@@ -36,7 +36,7 @@ const props = defineProps<{
 
 	&:hover {
 		text-decoration: none;
-		color: var(--accent);
+		color: var(--MI_THEME-accent);
 	}
 
 	&:focus-visible {
@@ -92,7 +92,7 @@ const props = defineProps<{
 	}
 
 	&:global(.gray) {
-		--c: var(--bg);
+		--c: var(--MI_THEME-bg);
 		background-image: linear-gradient(45deg, var(--c) 16.67%, transparent 16.67%, transparent 50%, var(--c) 50%, var(--c) 66.67%, transparent 66.67%, transparent 100%);
 		background-size: 16px 16px;
 	}
diff --git a/packages/frontend/src/components/MkFoldableSection.vue b/packages/frontend/src/components/MkFoldableSection.vue
index f10d58b38a..ef1d075360 100644
--- a/packages/frontend/src/components/MkFoldableSection.vue
+++ b/packages/frontend/src/components/MkFoldableSection.vue
@@ -83,7 +83,7 @@ function afterLeave(element: Element) {
 
 onMounted(() => {
 	function getParentBg(el?: HTMLElement | null): string {
-		if (el == null || el.tagName === 'BODY') return 'var(--bg)';
+		if (el == null || el.tagName === 'BODY') return 'var(--MI_THEME-bg)';
 		const background = el.style.background || el.style.backgroundColor;
 		if (background) {
 			return background;
@@ -134,7 +134,7 @@ onMounted(() => {
 	flex: 1;
 	margin: auto;
 	height: 1px;
-	background: var(--divider);
+	background: var(--MI_THEME-divider);
 }
 
 .button {
diff --git a/packages/frontend/src/components/MkFolder.vue b/packages/frontend/src/components/MkFolder.vue
index 8262ae5d0c..290d73dd92 100644
--- a/packages/frontend/src/components/MkFolder.vue
+++ b/packages/frontend/src/components/MkFolder.vue
@@ -118,7 +118,7 @@ function toggle() {
 onMounted(() => {
 	const computedStyle = getComputedStyle(document.documentElement);
 	const parentBg = getBgColor(rootEl.value!.parentElement!);
-	const myBg = computedStyle.getPropertyValue('--panel');
+	const myBg = computedStyle.getPropertyValue('--MI_THEME-panel');
 	bgSame.value = parentBg === myBg;
 });
 </script>
@@ -144,7 +144,7 @@ onMounted(() => {
 	width: 100%;
 	box-sizing: border-box;
 	padding: 9px 12px 9px 12px;
-	background: var(--folderHeaderBg);
+	background: var(--MI_THEME-folderHeaderBg);
 	-webkit-backdrop-filter: var(--blur, blur(15px));
 	backdrop-filter: var(--blur, blur(15px));
 	border-radius: 6px;
@@ -152,7 +152,7 @@ onMounted(() => {
 
 	&:hover {
 		text-decoration: none;
-		background: var(--folderHeaderHoverBg);
+		background: var(--MI_THEME-folderHeaderHoverBg);
 	}
 
 	&:focus-within {
@@ -160,8 +160,8 @@ onMounted(() => {
 	}
 
 	&.active {
-		color: var(--accent);
-		background: var(--folderHeaderHoverBg);
+		color: var(--MI_THEME-accent);
+		background: var(--MI_THEME-folderHeaderHoverBg);
 	}
 
 	&.opened {
@@ -175,7 +175,7 @@ onMounted(() => {
 }
 
 .headerLower {
-	color: var(--fgTransparentWeak);
+	color: var(--MI_THEME-fgTransparentWeak);
 	font-size: .85em;
 	padding-left: 4px;
 }
@@ -209,13 +209,13 @@ onMounted(() => {
 }
 
 .headerTextSub {
-	color: var(--fgTransparentWeak);
+	color: var(--MI_THEME-fgTransparentWeak);
 	font-size: .85em;
 }
 
 .headerRight {
 	margin-left: auto;
-	color: var(--fgTransparentWeak);
+	color: var(--MI_THEME-fgTransparentWeak);
 	white-space: nowrap;
 }
 
@@ -224,12 +224,12 @@ onMounted(() => {
 }
 
 .body {
-	background: var(--panel);
+	background: var(--MI_THEME-panel);
 	border-radius: 0 0 6px 6px;
 	container-type: inline-size;
 
 	&.bgSame {
-		background: var(--bg);
+		background: var(--MI_THEME-bg);
 	}
 }
 
@@ -239,11 +239,11 @@ onMounted(() => {
 	bottom: var(--stickyBottom, 0px);
 	left: 0;
 	padding: 12px;
-	background: var(--acrylicBg);
+	background: var(--MI_THEME-acrylicBg);
 	-webkit-backdrop-filter: var(--blur, blur(15px));
 	backdrop-filter: var(--blur, blur(15px));
 	background-size: auto auto;
-	background-image: repeating-linear-gradient(135deg, transparent, transparent 5px, var(--panel) 5px, var(--panel) 10px);
+	background-image: repeating-linear-gradient(135deg, transparent, transparent 5px, var(--MI_THEME-panel) 5px, var(--MI_THEME-panel) 10px);
 	border-radius: 0 0 6px 6px;
 }
 </style>
diff --git a/packages/frontend/src/components/MkFollowButton.vue b/packages/frontend/src/components/MkFollowButton.vue
index 0de52906ed..ccea7cd453 100644
--- a/packages/frontend/src/components/MkFollowButton.vue
+++ b/packages/frontend/src/components/MkFollowButton.vue
@@ -165,8 +165,8 @@ onBeforeUnmount(() => {
 	position: relative;
 	display: inline-block;
 	font-weight: bold;
-	color: var(--fgOnWhite);
-	border: solid 1px var(--accent);
+	color: var(--MI_THEME-fgOnWhite);
+	border: solid 1px var(--MI_THEME-accent);
 	padding: 0;
 	height: 31px;
 	font-size: 16px;
@@ -201,17 +201,17 @@ onBeforeUnmount(() => {
 	}
 
 	&.active {
-		color: var(--fgOnAccent);
-		background: var(--accent);
+		color: var(--MI_THEME-fgOnAccent);
+		background: var(--MI_THEME-accent);
 
 		&:hover {
-			background: var(--accentLighten);
-			border-color: var(--accentLighten);
+			background: var(--MI_THEME-accentLighten);
+			border-color: var(--MI_THEME-accentLighten);
 		}
 
 		&:active {
-			background: var(--accentDarken);
-			border-color: var(--accentDarken);
+			background: var(--MI_THEME-accentDarken);
+			border-color: var(--MI_THEME-accentDarken);
 		}
 	}
 
diff --git a/packages/frontend/src/components/MkFormDialog.file.vue b/packages/frontend/src/components/MkFormDialog.file.vue
index 9360594236..ecb6cf882b 100644
--- a/packages/frontend/src/components/MkFormDialog.file.vue
+++ b/packages/frontend/src/components/MkFormDialog.file.vue
@@ -66,6 +66,6 @@ function selectButton(ev: MouseEvent) {
 <style module>
 .fileNotSelected {
 	font-weight: 700;
-	color: var(--infoWarnFg);
+	color: var(--MI_THEME-infoWarnFg);
 }
 </style>
diff --git a/packages/frontend/src/components/MkFormFooter.vue b/packages/frontend/src/components/MkFormFooter.vue
index 1e88d59d8e..f409f6ce50 100644
--- a/packages/frontend/src/components/MkFormFooter.vue
+++ b/packages/frontend/src/components/MkFormFooter.vue
@@ -36,7 +36,7 @@ const props = defineProps<{
 }
 
 .text {
-	color: var(--warn);
+	color: var(--MI_THEME-warn);
 	font-size: 90%;
 	animation: modified-blink 2s infinite;
 }
diff --git a/packages/frontend/src/components/MkFukidashi.vue b/packages/frontend/src/components/MkFukidashi.vue
index 09825487bf..307cd15dc8 100644
--- a/packages/frontend/src/components/MkFukidashi.vue
+++ b/packages/frontend/src/components/MkFukidashi.vue
@@ -40,7 +40,7 @@ withDefaults(defineProps<{
 <style module lang="scss">
 .root {
 	--fukidashi-radius: var(--radius);
-	--fukidashi-bg: var(--panel);
+	--fukidashi-bg: var(--MI_THEME-panel);
 
 	position: relative;
 	display: inline-block;
@@ -48,7 +48,7 @@ withDefaults(defineProps<{
 	padding-top: calc(var(--fukidashi-radius) * .13);
 
 	&.shadow {
-		filter: drop-shadow(0 4px 32px var(--shadow));
+		filter: drop-shadow(0 4px 32px var(--MI_THEME-shadow));
 	}
 
 	&.left {
diff --git a/packages/frontend/src/components/MkGalleryPostPreview.vue b/packages/frontend/src/components/MkGalleryPostPreview.vue
index 2bb5b8762a..22f8355acf 100644
--- a/packages/frontend/src/components/MkGalleryPostPreview.vue
+++ b/packages/frontend/src/components/MkGalleryPostPreview.vue
@@ -75,7 +75,7 @@ function leaveHover(): void {
 
 	&:hover {
 		text-decoration: none;
-		color: var(--accent);
+		color: var(--MI_THEME-accent);
 
 		> .thumbnail {
 			transform: scale(1.1);
diff --git a/packages/frontend/src/components/MkGoogle.vue b/packages/frontend/src/components/MkGoogle.vue
index 2988d77fe3..2eaee5b115 100644
--- a/packages/frontend/src/components/MkGoogle.vue
+++ b/packages/frontend/src/components/MkGoogle.vue
@@ -39,7 +39,7 @@ const search = () => {
 	width: 100%;
 	height: 40px;
 	font-size: 16px;
-	border: solid 1px var(--divider);
+	border: solid 1px var(--MI_THEME-divider);
 	border-radius: 4px 0 0 4px;
 	-webkit-appearance: textfield;
 }
@@ -48,7 +48,7 @@ const search = () => {
 	flex-shrink: 0;
 	margin: 0;
 	padding: 0 16px;
-	border: solid 1px var(--divider);
+	border: solid 1px var(--MI_THEME-divider);
 	border-left: none;
 	border-radius: 0 4px 4px 0;
 
diff --git a/packages/frontend/src/components/MkInfo.vue b/packages/frontend/src/components/MkInfo.vue
index 33e65ccc4e..87c98cf072 100644
--- a/packages/frontend/src/components/MkInfo.vue
+++ b/packages/frontend/src/components/MkInfo.vue
@@ -36,14 +36,14 @@ function close() {
   align-items: center;
 	padding: 12px 14px;
 	font-size: 90%;
-	background: var(--infoBg);
-	color: var(--infoFg);
+	background: var(--MI_THEME-infoBg);
+	color: var(--MI_THEME-infoFg);
 	border-radius: var(--radius);
 	white-space: pre-wrap;
 
 	&.warn {
-		background: var(--infoWarnBg);
-		color: var(--infoWarnFg);
+		background: var(--MI_THEME-infoWarnBg);
+		color: var(--MI_THEME-infoWarnFg);
 	}
 }
 
diff --git a/packages/frontend/src/components/MkInput.vue b/packages/frontend/src/components/MkInput.vue
index 4c2fc1ba00..e01ff86c5a 100644
--- a/packages/frontend/src/components/MkInput.vue
+++ b/packages/frontend/src/components/MkInput.vue
@@ -199,7 +199,7 @@ defineExpose({
 .caption {
 	font-size: 0.85em;
 	padding: 8px 0 0 0;
-	color: var(--fgTransparentWeak);
+	color: var(--MI_THEME-fgTransparentWeak);
 
 	&:empty {
 		display: none;
@@ -216,8 +216,8 @@ defineExpose({
 
 	&.focused {
 		> .inputCore {
-			border-color: var(--accent) !important;
-			//box-shadow: 0 0 0 4px var(--focus);
+			border-color: var(--MI_THEME-accent) !important;
+			//box-shadow: 0 0 0 4px var(--MI_THEME-focus);
 		}
 	}
 
@@ -242,9 +242,9 @@ defineExpose({
 	font: inherit;
 	font-weight: normal;
 	font-size: 1em;
-	color: var(--fg);
-	background: var(--panel);
-	border: solid 1px var(--panel);
+	color: var(--MI_THEME-fg);
+	background: var(--MI_THEME-panel);
+	border: solid 1px var(--MI_THEME-panel);
 	border-radius: 6px;
 	outline: none;
 	box-shadow: none;
@@ -252,7 +252,7 @@ defineExpose({
 	transition: border-color 0.1s ease-out;
 
 	&:hover {
-		border-color: var(--inputBorderHover) !important;
+		border-color: var(--MI_THEME-inputBorderHover) !important;
 	}
 }
 
diff --git a/packages/frontend/src/components/MkInstanceCardMini.vue b/packages/frontend/src/components/MkInstanceCardMini.vue
index 17c974dd04..b0601cf7f9 100644
--- a/packages/frontend/src/components/MkInstanceCardMini.vue
+++ b/packages/frontend/src/components/MkInstanceCardMini.vue
@@ -46,7 +46,7 @@ function getInstanceIcon(instance): string {
 	display: flex;
 	align-items: center;
 	padding: 16px;
-	background: var(--panel);
+	background: var(--MI_THEME-panel);
 	border-radius: 8px;
 
 	> :global(.icon) {
@@ -62,7 +62,7 @@ function getInstanceIcon(instance): string {
 		flex: 1;
 		overflow: hidden;
 		font-size: 0.9em;
-		color: var(--fg);
+		color: var(--MI_THEME-fg);
 		padding-right: 8px;
 
 		> :global(.host) {
@@ -109,7 +109,7 @@ function getInstanceIcon(instance): string {
 	}
 
 	&:global(.gray) {
-		--c: var(--bg);
+		--c: var(--MI_THEME-bg);
 		background-image: linear-gradient(45deg, var(--c) 16.67%, transparent 16.67%, transparent 50%, var(--c) 50%, var(--c) 66.67%, transparent 66.67%, transparent 100%);
 		background-size: 16px 16px;
 	}
diff --git a/packages/frontend/src/components/MkInstanceStats.vue b/packages/frontend/src/components/MkInstanceStats.vue
index d74c885041..da313d4d70 100644
--- a/packages/frontend/src/components/MkInstanceStats.vue
+++ b/packages/frontend/src/components/MkInstanceStats.vue
@@ -121,7 +121,7 @@ function createDoughnut(chartEl, tooltip, data) {
 			labels: data.map(x => x.name),
 			datasets: [{
 				backgroundColor: data.map(x => x.color),
-				borderColor: getComputedStyle(document.documentElement).getPropertyValue('--panel'),
+				borderColor: getComputedStyle(document.documentElement).getPropertyValue('--MI_THEME-panel'),
 				borderWidth: 2,
 				hoverOffset: 0,
 				data: data.map(x => x.value),
@@ -256,7 +256,7 @@ onMounted(() => {
 				flex: 1;
 				min-width: 0;
 				position: relative;
-				background: var(--panel);
+				background: var(--MI_THEME-panel);
 				border-radius: var(--radius);
 				padding: 24px;
 				max-height: 300px;
diff --git a/packages/frontend/src/components/MkInviteCode.vue b/packages/frontend/src/components/MkInviteCode.vue
index 4aee64f78e..1a71f6574f 100644
--- a/packages/frontend/src/components/MkInviteCode.vue
+++ b/packages/frontend/src/components/MkInviteCode.vue
@@ -8,8 +8,8 @@ SPDX-License-Identifier: AGPL-3.0-only
 	<template #label>{{ invite.code }}</template>
 	<template #suffix>
 		<span v-if="invite.used">{{ i18n.ts.used }}</span>
-		<span v-else-if="isExpired" style="color: var(--error)">{{ i18n.ts.expired }}</span>
-		<span v-else style="color: var(--success)">{{ i18n.ts.unused }}</span>
+		<span v-else-if="isExpired" style="color: var(--MI_THEME-error)">{{ i18n.ts.expired }}</span>
+		<span v-else style="color: var(--MI_THEME-success)">{{ i18n.ts.unused }}</span>
 	</template>
 	<template #footer>
 		<div class="_buttons">
diff --git a/packages/frontend/src/components/MkLaunchPad.vue b/packages/frontend/src/components/MkLaunchPad.vue
index 8e3c19bd12..2dcba7a50e 100644
--- a/packages/frontend/src/components/MkLaunchPad.vue
+++ b/packages/frontend/src/components/MkLaunchPad.vue
@@ -105,8 +105,8 @@ function close() {
 			box-sizing: border-box;
 
 			&:hover {
-				color: var(--accent);
-				background: var(--accentedBg);
+				color: var(--MI_THEME-accent);
+				background: var(--MI_THEME-accentedBg);
 				text-decoration: none;
 			}
 
@@ -137,7 +137,7 @@ function close() {
 				position: absolute;
 				top: 32px;
 				left: 32px;
-				color: var(--indicator);
+				color: var(--MI_THEME-indicator);
 				font-size: 8px;
 				animation: global-blink 1s infinite;
 
diff --git a/packages/frontend/src/components/MkMediaAudio.vue b/packages/frontend/src/components/MkMediaAudio.vue
index b41705d5e6..915d67db7f 100644
--- a/packages/frontend/src/components/MkMediaAudio.vue
+++ b/packages/frontend/src/components/MkMediaAudio.vue
@@ -391,7 +391,7 @@ onDeactivated(() => {
 .audioContainer {
 	container-type: inline-size;
 	position: relative;
-	border: .5px solid var(--divider);
+	border: .5px solid var(--MI_THEME-divider);
 	border-radius: var(--radius);
 	overflow: clip;
 
@@ -412,7 +412,7 @@ onDeactivated(() => {
 		height: 100%;
 		pointer-events: none;
 		border-radius: inherit;
-		box-shadow: inset 0 0 0 4px var(--warn);
+		box-shadow: inset 0 0 0 4px var(--MI_THEME-warn);
 	}
 }
 
@@ -458,8 +458,8 @@ onDeactivated(() => {
 		font-size: 1.05rem;
 
 		&:hover {
-			color: var(--accent);
-			background-color: var(--accentedBg);
+			color: var(--MI_THEME-accent);
+			background-color: var(--MI_THEME-accentedBg);
 		}
 
 		&:focus-visible {
diff --git a/packages/frontend/src/components/MkMediaImage.vue b/packages/frontend/src/components/MkMediaImage.vue
index 0c5c8fd9de..fbd973c196 100644
--- a/packages/frontend/src/components/MkMediaImage.vue
+++ b/packages/frontend/src/components/MkMediaImage.vue
@@ -42,7 +42,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 		<div :class="$style.indicators">
 			<div v-if="['image/gif', 'image/apng'].includes(image.type)" :class="$style.indicator">GIF</div>
 			<div v-if="image.comment" :class="$style.indicator">ALT</div>
-			<div v-if="image.isSensitive" :class="$style.indicator" style="color: var(--warn);" :title="i18n.ts.sensitive"><i class="ti ti-eye-exclamation"></i></div>
+			<div v-if="image.isSensitive" :class="$style.indicator" style="color: var(--MI_THEME-warn);" :title="i18n.ts.sensitive"><i class="ti ti-eye-exclamation"></i></div>
 		</div>
 		<button :class="$style.menu" class="_button" @click.stop="showMenu"><i class="ti ti-dots" style="vertical-align: middle;"></i></button>
 		<i class="ti ti-eye-off" :class="$style.hide" @click.stop="hide = true"></i>
@@ -165,7 +165,7 @@ function showMenu(ev: MouseEvent) {
 		height: 100%;
 		pointer-events: none;
 		border-radius: inherit;
-		box-shadow: inset 0 0 0 4px var(--warn);
+		box-shadow: inset 0 0 0 4px var(--MI_THEME-warn);
 	}
 }
 
@@ -186,8 +186,8 @@ function showMenu(ev: MouseEvent) {
 	display: block;
 	position: absolute;
 	border-radius: 6px;
-	background-color: var(--fg);
-	color: var(--accentLighten);
+	background-color: var(--MI_THEME-fg);
+	color: var(--MI_THEME-accentLighten);
 	font-size: 12px;
 	opacity: .5;
 	padding: 5px 8px;
@@ -206,19 +206,19 @@ function showMenu(ev: MouseEvent) {
 
 .visible {
 	position: relative;
-	//box-shadow: 0 0 0 1px var(--divider) inset;
-	background: var(--bg);
+	//box-shadow: 0 0 0 1px var(--MI_THEME-divider) inset;
+	background: var(--MI_THEME-bg);
 	background-size: 16px 16px;
 }
 
 html[data-color-scheme=dark] .visible {
 	--c: rgb(255 255 255 / 2%);
-	background-image: linear-gradient(45deg, var(--c) 16.67%, var(--bg) 16.67%, var(--bg) 50%, var(--c) 50%, var(--c) 66.67%, var(--bg) 66.67%, var(--bg) 100%);
+	background-image: linear-gradient(45deg, var(--c) 16.67%, var(--MI_THEME-bg) 16.67%, var(--MI_THEME-bg) 50%, var(--c) 50%, var(--c) 66.67%, var(--MI_THEME-bg) 66.67%, var(--MI_THEME-bg) 100%);
 }
 
 html[data-color-scheme=light] .visible {
 	--c: rgb(0 0 0 / 2%);
-	background-image: linear-gradient(45deg, var(--c) 16.67%, var(--bg) 16.67%, var(--bg) 50%, var(--c) 50%, var(--c) 66.67%, var(--bg) 66.67%, var(--bg) 100%);
+	background-image: linear-gradient(45deg, var(--c) 16.67%, var(--MI_THEME-bg) 16.67%, var(--MI_THEME-bg) 50%, var(--c) 50%, var(--c) 66.67%, var(--MI_THEME-bg) 66.67%, var(--MI_THEME-bg) 100%);
 }
 
 .menu {
@@ -258,10 +258,10 @@ html[data-color-scheme=light] .visible {
 }
 
 .indicator {
-	/* Hardcode to black because either --bg or --fg makes it hard to read in dark/light mode */
+	/* Hardcode to black because either --MI_THEME-bg or --MI_THEME-fg makes it hard to read in dark/light mode */
 	background-color: black;
 	border-radius: 6px;
-	color: var(--accentLighten);
+	color: var(--MI_THEME-accentLighten);
 	display: inline-block;
 	font-weight: bold;
 	font-size: 0.8em;
diff --git a/packages/frontend/src/components/MkMediaList.vue b/packages/frontend/src/components/MkMediaList.vue
index 4a4a99be25..9fab73d87b 100644
--- a/packages/frontend/src/components/MkMediaList.vue
+++ b/packages/frontend/src/components/MkMediaList.vue
@@ -310,13 +310,13 @@ defineExpose({
 
 :global(.pswp) {
 	--pswp-root-z-index: var(--mk-pswp-root-z-index, 2000700) !important;
-	--pswp-bg: var(--modalBg) !important;
+	--pswp-bg: var(--MI_THEME-modalBg) !important;
 }
 </style>
 
 <style lang="scss">
 .pswp__bg {
-	background: var(--modalBg);
+	background: var(--MI_THEME-modalBg);
 	backdrop-filter: var(--modalBgFilter);
 }
 
@@ -335,14 +335,14 @@ defineExpose({
 }
 
 .pswp__alt-text {
-	color: var(--fg);
+	color: var(--MI_THEME-fg);
 	margin: 0 auto;
 	text-align: center;
 	padding: var(--margin);
 	border-radius: var(--radius);
 	max-height: 8em;
 	overflow-y: auto;
-	text-shadow: var(--bg) 0 0 10px, var(--bg) 0 0 3px, var(--bg) 0 0 3px;
+	text-shadow: var(--MI_THEME-bg) 0 0 10px, var(--MI_THEME-bg) 0 0 3px, var(--MI_THEME-bg) 0 0 3px;
 	white-space: pre-line;
 }
 </style>
diff --git a/packages/frontend/src/components/MkMediaRange.vue b/packages/frontend/src/components/MkMediaRange.vue
index 86ed8ba2cf..df7505b0c3 100644
--- a/packages/frontend/src/components/MkMediaRange.vue
+++ b/packages/frontend/src/components/MkMediaRange.vue
@@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <!-- Media系専用のinput range -->
 <template>
-<div :style="sliderBgWhite ? '--sliderBg: rgba(255,255,255,.25);' : '--sliderBg: var(--scrollbarHandle);'">
+<div :style="sliderBgWhite ? '--sliderBg: rgba(255,255,255,.25);' : '--sliderBg: var(--MI_THEME-scrollbarHandle);'">
 	<div :class="$style.controlsSeekbar">
 		<progress v-if="buffer !== undefined" :class="$style.buffer" :value="isNaN(buffer) ? 0 : buffer" min="0" max="1">{{ Math.round(buffer * 100) }}% buffered</progress>
 		<input v-model="model" :class="$style.seek" :style="`--value: ${modelValue * 100}%;`" type="range" min="0" max="1" step="any" @change="emit('dragEnded', modelValue)"/>
@@ -48,7 +48,7 @@ const modelValue = computed({
 	background: transparent;
 	border: 0;
 	border-radius: 26px;
-	color: var(--accent);
+	color: var(--MI_THEME-accent);
 	display: block;
 	height: 19px;
 	margin: 0;
diff --git a/packages/frontend/src/components/MkMediaVideo.vue b/packages/frontend/src/components/MkMediaVideo.vue
index 1b1915e6c8..271c66552b 100644
--- a/packages/frontend/src/components/MkMediaVideo.vue
+++ b/packages/frontend/src/components/MkMediaVideo.vue
@@ -42,7 +42,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 		<i class="ti ti-eye-off" :class="$style.hide" @click="hide = true"></i>
 		<div :class="$style.indicators">
 			<div v-if="video.comment" :class="$style.indicator">ALT</div>
-			<div v-if="video.isSensitive" :class="$style.indicator" style="color: var(--warn);" :title="i18n.ts.sensitive"><i class="ti ti-eye-exclamation"></i></div>
+			<div v-if="video.isSensitive" :class="$style.indicator" style="color: var(--MI_THEME-warn);" :title="i18n.ts.sensitive"><i class="ti ti-eye-exclamation"></i></div>
 		</div>
 	</div>
 
@@ -67,7 +67,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 		<i class="ti ti-eye-off" :class="$style.hide" @click="hide = true"></i>
 		<div :class="$style.indicators">
 			<div v-if="video.comment" :class="$style.indicator">ALT</div>
-			<div v-if="video.isSensitive" :class="$style.indicator" style="color: var(--warn);" :title="i18n.ts.sensitive"><i class="ti ti-eye-exclamation"></i></div>
+			<div v-if="video.isSensitive" :class="$style.indicator" style="color: var(--MI_THEME-warn);" :title="i18n.ts.sensitive"><i class="ti ti-eye-exclamation"></i></div>
 		</div>
 		<div :class="$style.videoControls" @click.self="togglePlayPause">
 			<div :class="[$style.controlsChild, $style.controlsLeft]">
@@ -508,7 +508,7 @@ onDeactivated(() => {
 		height: 100%;
 		pointer-events: none;
 		border-radius: inherit;
-		box-shadow: inset 0 0 0 4px var(--warn);
+		box-shadow: inset 0 0 0 4px var(--MI_THEME-warn);
 	}
 }
 
@@ -523,10 +523,10 @@ onDeactivated(() => {
 }
 
 .indicator {
-	/* Hardcode to black because either --bg or --fg makes it hard to read in dark/light mode */
+	/* Hardcode to black because either --MI_THEME-bg or --MI_THEME-fg makes it hard to read in dark/light mode */
 	background-color: black;
 	border-radius: 6px;
-	color: var(--accentLighten);
+	color: var(--MI_THEME-accentLighten);
 	display: inline-block;
 	font-weight: bold;
 	font-size: 0.8em;
@@ -537,8 +537,8 @@ onDeactivated(() => {
 	display: block;
 	position: absolute;
 	border-radius: 6px;
-	background-color: var(--fg);
-	color: var(--accentLighten);
+	background-color: var(--MI_THEME-fg);
+	color: var(--MI_THEME-accentLighten);
 	font-size: 12px;
 	opacity: .5;
 	padding: 5px 8px;
@@ -592,7 +592,7 @@ onDeactivated(() => {
 	opacity: 0;
 	transition: opacity .4s ease-in-out;
 
-	background: var(--accent);
+	background: var(--MI_THEME-accent);
 	color: #fff;
 	padding: 1rem;
 	border-radius: 99rem;
@@ -663,7 +663,7 @@ onDeactivated(() => {
 		font-size: 1.05rem;
 
 		&:hover {
-			background-color: var(--accent);
+			background-color: var(--MI_THEME-accent);
 		}
 
 		&:focus-visible {
diff --git a/packages/frontend/src/components/MkMention.vue b/packages/frontend/src/components/MkMention.vue
index 71bd5addfb..ac2d3f4398 100644
--- a/packages/frontend/src/components/MkMention.vue
+++ b/packages/frontend/src/components/MkMention.vue
@@ -47,12 +47,12 @@ const avatarUrl = computed(() => defaultStore.state.disableShowingAnimatedImages
 	display: inline-block;
 	padding: 4px 8px 4px 4px;
 	border-radius: 999px;
-	color: var(--mention);
-	background: color(from var(--mention) srgb r g b / 0.1);
+	color: var(--MI_THEME-mention);
+	background: color(from var(--MI_THEME-mention) srgb r g b / 0.1);
 
 	&.isMe {
-		color: var(--mentionMe);
-		background: color(from var(--mentionMe) srgb r g b / 0.1);
+		color: var(--MI_THEME-mentionMe);
+		background: color(from var(--MI_THEME-mentionMe) srgb r g b / 0.1);
 	}
 }
 
diff --git a/packages/frontend/src/components/MkMenu.vue b/packages/frontend/src/components/MkMenu.vue
index 14f6bdcc34..59f36f8eec 100644
--- a/packages/frontend/src/components/MkMenu.vue
+++ b/packages/frontend/src/components/MkMenu.vue
@@ -507,7 +507,7 @@ onBeforeUnmount(() => {
 	overflow: hidden;
 	text-overflow: ellipsis;
 	text-decoration: none !important;
-	color: var(--menuFg, var(--fg));
+	color: var(--menuFg, var(--MI_THEME-fg));
 
 	&::before {
 		content: "";
@@ -527,7 +527,7 @@ onBeforeUnmount(() => {
 		outline: none;
 
 		&:not(:hover):not(:active)::before {
-			outline: var(--focus) solid 2px;
+			outline: var(--MI_THEME-focus) solid 2px;
 			outline-offset: -2px;
 		}
 	}
@@ -536,19 +536,19 @@ onBeforeUnmount(() => {
 		&:hover,
 		&:focus-visible:active,
 		&:focus-visible.active {
-			color: var(--menuHoverFg, var(--accent));
+			color: var(--menuHoverFg, var(--MI_THEME-accent));
 
 			&::before {
-				background-color: var(--menuHoverBg, var(--accentedBg));
+				background-color: var(--menuHoverBg, var(--MI_THEME-accentedBg));
 			}
 		}
 
 		&:not(:focus-visible):active,
 		&:not(:focus-visible).active {
-			color: var(--menuActiveFg, var(--fgOnAccent));
+			color: var(--menuActiveFg, var(--MI_THEME-fgOnAccent));
 
 			&::before {
-				background-color: var(--menuActiveBg, var(--accent));
+				background-color: var(--menuActiveBg, var(--MI_THEME-accent));
 			}
 		}
 	}
@@ -566,13 +566,13 @@ onBeforeUnmount(() => {
 	}
 
 	&.radio {
-		--menuActiveFg: var(--accent);
-		--menuActiveBg: var(--accentedBg);
+		--menuActiveFg: var(--MI_THEME-accent);
+		--menuActiveBg: var(--MI_THEME-accentedBg);
 	}
 
 	&.parent {
-		--menuActiveFg: var(--accent);
-		--menuActiveBg: var(--accentedBg);
+		--menuActiveFg: var(--MI_THEME-accent);
+		--menuActiveBg: var(--MI_THEME-accentedBg);
 	}
 
 	&.label {
@@ -637,14 +637,14 @@ onBeforeUnmount(() => {
 .indicator {
 	display: flex;
 	align-items: center;
-	color: var(--indicator);
+	color: var(--MI_THEME-indicator);
 	font-size: 12px;
 	animation: global-blink 1s infinite;
 }
 
 .divider {
 	margin: 8px 0;
-	border-top: solid 0.5px var(--divider);
+	border-top: solid 0.5px var(--MI_THEME-divider);
 }
 
 .radioIcon {
@@ -654,11 +654,11 @@ onBeforeUnmount(() => {
 	height: 1em;
 	vertical-align: -0.125em;
 	border-radius: 50%;
-	border: solid 2px var(--divider);
-	background-color: var(--panel);
+	border: solid 2px var(--MI_THEME-divider);
+	background-color: var(--MI_THEME-panel);
 
 	&.radioChecked {
-		border-color: var(--accent);
+		border-color: var(--MI_THEME-accent);
 
 		&::after {
 			content: "";
@@ -670,7 +670,7 @@ onBeforeUnmount(() => {
 			width: 50%;
 			height: 50%;
 			border-radius: 50%;
-			background-color: var(--accent);
+			background-color: var(--MI_THEME-accent);
 		}
 	}
 }
diff --git a/packages/frontend/src/components/MkMiniChart.vue b/packages/frontend/src/components/MkMiniChart.vue
index 1b6f6cef31..7ea585ecc2 100644
--- a/packages/frontend/src/components/MkMiniChart.vue
+++ b/packages/frontend/src/components/MkMiniChart.vue
@@ -48,7 +48,7 @@ const polygonPoints = ref('');
 const headX = ref<number | null>(null);
 const headY = ref<number | null>(null);
 const clock = ref<number | null>(null);
-const accent = tinycolor(getComputedStyle(document.documentElement).getPropertyValue('--accent'));
+const accent = tinycolor(getComputedStyle(document.documentElement).getPropertyValue('--MI_THEME-accent'));
 const color = accent.toRgbString();
 
 function draw(): void {
diff --git a/packages/frontend/src/components/MkModalWindow.vue b/packages/frontend/src/components/MkModalWindow.vue
index f26959888b..c77611ef12 100644
--- a/packages/frontend/src/components/MkModalWindow.vue
+++ b/packages/frontend/src/components/MkModalWindow.vue
@@ -94,8 +94,8 @@ defineExpose({
 
 	--root-margin: 24px;
 
-	--headerHeight: 46px;
-	--headerHeightNarrow: 42px;
+	--MI_THEME-headerHeight: 46px;
+	--MI_THEME-headerHeightNarrow: 42px;
 
 	@media (max-width: 500px) {
 		--root-margin: 16px;
@@ -105,24 +105,24 @@ defineExpose({
 .header {
 	display: flex;
 	flex-shrink: 0;
-	background: var(--windowHeader);
+	background: var(--MI_THEME-windowHeader);
 	-webkit-backdrop-filter: var(--blur, blur(15px));
 	backdrop-filter: var(--blur, blur(15px));
 }
 
 .headerButton {
-	height: var(--headerHeight);
-	width: var(--headerHeight);
+	height: var(--MI_THEME-headerHeight);
+	width: var(--MI_THEME-headerHeight);
 
 	@media (max-width: 500px) {
-		height: var(--headerHeightNarrow);
-		width: var(--headerHeightNarrow);
+		height: var(--MI_THEME-headerHeightNarrow);
+		width: var(--MI_THEME-headerHeightNarrow);
 	}
 }
 
 .title {
 	flex: 1;
-	line-height: var(--headerHeight);
+	line-height: var(--MI_THEME-headerHeight);
 	padding-left: 32px;
 	font-weight: bold;
 	white-space: nowrap;
@@ -131,7 +131,7 @@ defineExpose({
 	pointer-events: none;
 
 	@media (max-width: 500px) {
-		line-height: var(--headerHeightNarrow);
+		line-height: var(--MI_THEME-headerHeightNarrow);
 		padding-left: 16px;
 	}
 }
@@ -143,7 +143,7 @@ defineExpose({
 .body {
 	flex: 1;
 	overflow: auto;
-	background: var(--panel);
+	background: var(--MI_THEME-panel);
 	container-type: size;
 }
 </style>
diff --git a/packages/frontend/src/components/MkNote.vue b/packages/frontend/src/components/MkNote.vue
index e8ff743bf2..c5f5431dcf 100644
--- a/packages/frontend/src/components/MkNote.vue
+++ b/packages/frontend/src/components/MkNote.vue
@@ -126,8 +126,8 @@ SPDX-License-Identifier: AGPL-3.0-only
 					<i class="ti ti-ban"></i>
 				</button>
 				<button ref="reactButton" :class="$style.footerButton" class="_button" @click="toggleReact()">
-					<i v-if="appearNote.reactionAcceptance === 'likeOnly' && appearNote.myReaction != null" class="ti ti-heart-filled" style="color: var(--love);"></i>
-					<i v-else-if="appearNote.myReaction != null" class="ti ti-minus" style="color: var(--accent);"></i>
+					<i v-if="appearNote.reactionAcceptance === 'likeOnly' && appearNote.myReaction != null" class="ti ti-heart-filled" style="color: var(--MI_THEME-love);"></i>
+					<i v-else-if="appearNote.myReaction != null" class="ti ti-minus" style="color: var(--MI_THEME-accent);"></i>
 					<i v-else-if="appearNote.reactionAcceptance === 'likeOnly'" class="ti ti-heart"></i>
 					<i v-else class="ti ti-plus"></i>
 					<p v-if="(appearNote.reactionAcceptance === 'likeOnly' || defaultStore.state.showReactionsCount) && appearNote.reactionCount > 0" :class="$style.footerButtonCount">{{ number(appearNote.reactionCount) }}</p>
@@ -643,7 +643,7 @@ function emitUpdReaction(emoji: string, delta: number) {
 			margin: auto;
 			width: calc(100% - 8px);
 			height: calc(100% - 8px);
-			border: dashed 2px var(--focus);
+			border: dashed 2px var(--MI_THEME-focus);
 			border-radius: var(--radius);
 			box-sizing: border-box;
 		}
@@ -666,9 +666,9 @@ function emitUpdReaction(emoji: string, delta: number) {
 			right: 12px;
 			padding: 0 4px;
 			margin-bottom: 0 !important;
-			background: var(--popup);
+			background: var(--MI_THEME-popup);
 			border-radius: 8px;
-			box-shadow: 0px 4px 32px var(--shadow);
+			box-shadow: 0px 4px 32px var(--MI_THEME-shadow);
 		}
 
 		.footerButton {
@@ -713,7 +713,7 @@ function emitUpdReaction(emoji: string, delta: number) {
 	padding: 16px 32px 8px 32px;
 	line-height: 28px;
 	white-space: pre;
-	color: var(--renote);
+	color: var(--MI_THEME-renote);
 
 	& + .article {
 		padding-top: 8px;
@@ -836,7 +836,7 @@ function emitUpdReaction(emoji: string, delta: number) {
 
 .showLessLabel {
 	display: inline-block;
-	background: var(--popup);
+	background: var(--MI_THEME-popup);
 	padding: 6px 10px;
 	font-size: 0.8em;
 	border-radius: 999px;
@@ -857,16 +857,16 @@ function emitUpdReaction(emoji: string, delta: number) {
 	z-index: 2;
 	width: 100%;
 	height: 64px;
-	background: linear-gradient(0deg, var(--panel), color(from var(--panel) srgb r g b / 0));
+	background: linear-gradient(0deg, var(--MI_THEME-panel), color(from var(--MI_THEME-panel) srgb r g b / 0));
 
 	&:hover > .collapsedLabel {
-		background: var(--panelHighlight);
+		background: var(--MI_THEME-panelHighlight);
 	}
 }
 
 .collapsedLabel {
 	display: inline-block;
-	background: var(--panel);
+	background: var(--MI_THEME-panel);
 	padding: 6px 10px;
 	font-size: 0.8em;
 	border-radius: 999px;
@@ -878,12 +878,12 @@ function emitUpdReaction(emoji: string, delta: number) {
 }
 
 .replyIcon {
-	color: var(--accent);
+	color: var(--MI_THEME-accent);
 	margin-right: 0.5em;
 }
 
 .translation {
-	border: solid 0.5px var(--divider);
+	border: solid 0.5px var(--MI_THEME-divider);
 	border-radius: var(--radius);
 	padding: 12px;
 	margin-top: 8px;
@@ -903,7 +903,7 @@ function emitUpdReaction(emoji: string, delta: number) {
 
 .quoteNote {
 	padding: 16px;
-	border: dashed 1px var(--renote);
+	border: dashed 1px var(--MI_THEME-renote);
 	border-radius: 8px;
 	overflow: clip;
 }
@@ -927,7 +927,7 @@ function emitUpdReaction(emoji: string, delta: number) {
 	}
 
 	&:hover {
-		color: var(--fgHighlighted);
+		color: var(--MI_THEME-fgHighlighted);
 	}
 }
 
diff --git a/packages/frontend/src/components/MkNoteDetailed.vue b/packages/frontend/src/components/MkNoteDetailed.vue
index bdb800b32a..8a7a98d23f 100644
--- a/packages/frontend/src/components/MkNoteDetailed.vue
+++ b/packages/frontend/src/components/MkNoteDetailed.vue
@@ -135,8 +135,8 @@ SPDX-License-Identifier: AGPL-3.0-only
 				<i class="ti ti-ban"></i>
 			</button>
 			<button ref="reactButton" :class="$style.noteFooterButton" class="_button" @click="toggleReact()">
-				<i v-if="appearNote.reactionAcceptance === 'likeOnly' && appearNote.myReaction != null" class="ti ti-heart-filled" style="color: var(--love);"></i>
-				<i v-else-if="appearNote.myReaction != null" class="ti ti-minus" style="color: var(--accent);"></i>
+				<i v-if="appearNote.reactionAcceptance === 'likeOnly' && appearNote.myReaction != null" class="ti ti-heart-filled" style="color: var(--MI_THEME-love);"></i>
+				<i v-else-if="appearNote.myReaction != null" class="ti ti-minus" style="color: var(--MI_THEME-accent);"></i>
 				<i v-else-if="appearNote.reactionAcceptance === 'likeOnly'" class="ti ti-heart"></i>
 				<i v-else class="ti ti-plus"></i>
 				<p v-if="(appearNote.reactionAcceptance === 'likeOnly' || defaultStore.state.showReactionsCount) && appearNote.reactionCount > 0" :class="$style.noteFooterButtonCount">{{ number(appearNote.reactionCount) }}</p>
@@ -569,7 +569,7 @@ function loadConversation() {
 			margin: auto;
 			width: calc(100% - 8px);
 			height: calc(100% - 8px);
-			border: dashed 2px var(--focus);
+			border: dashed 2px var(--MI_THEME-focus);
 			border-radius: var(--radius);
 			box-sizing: border-box;
 		}
@@ -591,7 +591,7 @@ function loadConversation() {
 	padding: 16px 32px 8px 32px;
 	line-height: 28px;
 	white-space: pre;
-	color: var(--renote);
+	color: var(--MI_THEME-renote);
 }
 
 .renoteAvatar {
@@ -671,7 +671,7 @@ function loadConversation() {
 	padding: 4px 6px;
 	font-size: 80%;
 	line-height: 1;
-	border: solid 0.5px var(--divider);
+	border: solid 0.5px var(--MI_THEME-divider);
 	border-radius: 4px;
 }
 
@@ -699,18 +699,18 @@ function loadConversation() {
 }
 
 .noteReplyTarget {
-	color: var(--accent);
+	color: var(--MI_THEME-accent);
 	margin-right: 0.5em;
 }
 
 .rn {
 	margin-left: 4px;
 	font-style: oblique;
-	color: var(--renote);
+	color: var(--MI_THEME-renote);
 }
 
 .translation {
-	border: solid 0.5px var(--divider);
+	border: solid 0.5px var(--MI_THEME-divider);
 	border-radius: var(--radius);
 	padding: 12px;
 	margin-top: 8px;
@@ -726,7 +726,7 @@ function loadConversation() {
 
 .quoteNote {
 	padding: 16px;
-	border: dashed 1px var(--renote);
+	border: dashed 1px var(--MI_THEME-renote);
 	border-radius: 8px;
 	overflow: clip;
 }
@@ -752,7 +752,7 @@ function loadConversation() {
 	}
 
 	&:hover {
-		color: var(--fgHighlighted);
+		color: var(--MI_THEME-fgHighlighted);
 	}
 }
 
@@ -762,17 +762,17 @@ function loadConversation() {
 	opacity: 0.7;
 
 	&.reacted {
-		color: var(--accent);
+		color: var(--MI_THEME-accent);
 	}
 }
 
 .reply:not(:first-child) {
-	border-top: solid 0.5px var(--divider);
+	border-top: solid 0.5px var(--MI_THEME-divider);
 }
 
 .tabs {
-	border-top: solid 0.5px var(--divider);
-	border-bottom: solid 0.5px var(--divider);
+	border-top: solid 0.5px var(--MI_THEME-divider);
+	border-bottom: solid 0.5px var(--MI_THEME-divider);
 	display: flex;
 }
 
@@ -784,7 +784,7 @@ function loadConversation() {
 }
 
 .tabActive {
-	border-bottom: solid 2px var(--accent);
+	border-bottom: solid 2px var(--MI_THEME-accent);
 }
 
 .tab_renotes {
@@ -804,12 +804,12 @@ function loadConversation() {
 
 .reactionTab {
 	padding: 4px 6px;
-	border: solid 1px var(--divider);
+	border: solid 1px var(--MI_THEME-divider);
 	border-radius: 6px;
 }
 
 .reactionTabActive {
-	border-color: var(--accent);
+	border-color: var(--MI_THEME-accent);
 }
 
 @container (max-width: 500px) {
diff --git a/packages/frontend/src/components/MkNoteHeader.vue b/packages/frontend/src/components/MkNoteHeader.vue
index a75b9ddd10..750e32a9ff 100644
--- a/packages/frontend/src/components/MkNoteHeader.vue
+++ b/packages/frontend/src/components/MkNoteHeader.vue
@@ -78,7 +78,7 @@ const mock = inject<boolean>('mock', false);
 	margin: 0 .5em 0 0;
 	padding: 1px 6px;
 	font-size: 80%;
-	border: solid 0.5px var(--divider);
+	border: solid 0.5px var(--MI_THEME-divider);
 	border-radius: 3px;
 }
 
diff --git a/packages/frontend/src/components/MkNoteSub.vue b/packages/frontend/src/components/MkNoteSub.vue
index 829b37e7a7..e4bade309b 100644
--- a/packages/frontend/src/components/MkNoteSub.vue
+++ b/packages/frontend/src/components/MkNoteSub.vue
@@ -135,7 +135,7 @@ if (props.detail) {
 }
 
 .reply, .more {
-	border-left: solid 0.5px var(--divider);
+	border-left: solid 0.5px var(--MI_THEME-divider);
 	margin-top: 10px;
 }
 
@@ -156,7 +156,7 @@ if (props.detail) {
 .muted {
 	text-align: center;
 	padding: 8px !important;
-	border: 1px solid var(--divider);
+	border: 1px solid var(--MI_THEME-divider);
 	margin: 8px 8px 0 8px;
 	border-radius: 8px;
 }
diff --git a/packages/frontend/src/components/MkNotes.vue b/packages/frontend/src/components/MkNotes.vue
index 0856c146ba..cb240160cf 100644
--- a/packages/frontend/src/components/MkNotes.vue
+++ b/packages/frontend/src/components/MkNotes.vue
@@ -56,16 +56,16 @@ defineExpose({
 .root {
 	&.noGap {
 		> .notes {
-			background: var(--panel);
+			background: var(--MI_THEME-panel);
 		}
 	}
 
 	&:not(.noGap) {
 		> .notes {
-			background: var(--bg);
+			background: var(--MI_THEME-bg);
 
 			.note {
-				background: var(--panel);
+				background: var(--MI_THEME-panel);
 				border-radius: var(--radius);
 			}
 		}
diff --git a/packages/frontend/src/components/MkNotification.vue b/packages/frontend/src/components/MkNotification.vue
index b27d883b85..bef425097e 100644
--- a/packages/frontend/src/components/MkNotification.vue
+++ b/packages/frontend/src/components/MkNotification.vue
@@ -224,7 +224,7 @@ function getActualReactedUsersCount(notification: Misskey.entities.Notification)
 	--eventFollow: #36aed2;
 	--eventRenote: #36d298;
 	--eventReply: #007aff;
-	--eventReactionHeart: var(--love);
+	--eventReactionHeart: var(--MI_THEME-love);
 	--eventReaction: #e99a0b;
 	--eventAchievement: #cb9a11;
 	--eventLogin: #007aff;
@@ -284,8 +284,8 @@ function getActualReactedUsersCount(notification: Misskey.entities.Notification)
 	height: 20px;
 	box-sizing: border-box;
 	border-radius: 100%;
-	background: var(--panel);
-	box-shadow: 0 0 0 3px var(--panel);
+	background: var(--MI_THEME-panel);
+	box-shadow: 0 0 0 3px var(--MI_THEME-panel);
 	font-size: 11px;
 	text-align: center;
 	color: #fff;
@@ -437,8 +437,8 @@ function getActualReactedUsersCount(notification: Misskey.entities.Notification)
 	height: 20px;
 	box-sizing: border-box;
 	border-radius: 100%;
-	background: var(--panel);
-	box-shadow: 0 0 0 3px var(--panel);
+	background: var(--MI_THEME-panel);
+	box-shadow: 0 0 0 3px var(--MI_THEME-panel);
 	font-size: 11px;
 	text-align: center;
 	color: #fff;
diff --git a/packages/frontend/src/components/MkNotifications.vue b/packages/frontend/src/components/MkNotifications.vue
index d67616e6b2..5a6ada474a 100644
--- a/packages/frontend/src/components/MkNotifications.vue
+++ b/packages/frontend/src/components/MkNotifications.vue
@@ -106,6 +106,6 @@ defineExpose({
 
 <style lang="scss" module>
 .list {
-	background: var(--panel);
+	background: var(--MI_THEME-panel);
 }
 </style>
diff --git a/packages/frontend/src/components/MkNumberDiff.vue b/packages/frontend/src/components/MkNumberDiff.vue
index 1825cc5405..80c634fdce 100644
--- a/packages/frontend/src/components/MkNumberDiff.vue
+++ b/packages/frontend/src/components/MkNumberDiff.vue
@@ -24,11 +24,11 @@ const isZero = computed(() => props.value === 0);
 
 <style lang="scss" module>
 .isPlus {
-	color: var(--success);
+	color: var(--MI_THEME-success);
 }
 
 .isMinus {
-	color: var(--error);
+	color: var(--MI_THEME-error);
 }
 
 .isZero {
diff --git a/packages/frontend/src/components/MkObjectView.value.vue b/packages/frontend/src/components/MkObjectView.value.vue
index 870599aa94..dabdd324fd 100644
--- a/packages/frontend/src/components/MkObjectView.value.vue
+++ b/packages/frontend/src/components/MkObjectView.value.vue
@@ -78,7 +78,7 @@ function collapsable(v): boolean {
 
 	> .boolean {
 		display: inline;
-		color: var(--codeBoolean);
+		color: var(--MI_THEME-codeBoolean);
 
 		&.true {
 			font-weight: bold;
@@ -91,12 +91,12 @@ function collapsable(v): boolean {
 
 	> .string {
 		display: inline;
-		color: var(--codeString);
+		color: var(--MI_THEME-codeString);
 	}
 
 	> .number {
 		display: inline;
-		color: var(--codeNumber);
+		color: var(--MI_THEME-codeNumber);
 	}
 
 	> .array.empty {
@@ -127,7 +127,7 @@ function collapsable(v): boolean {
 
 			> .toggle {
 				width: 16px;
-				color: var(--accent);
+				color: var(--MI_THEME-accent);
 				visibility: hidden;
 
 				&.visible {
diff --git a/packages/frontend/src/components/MkOmit.vue b/packages/frontend/src/components/MkOmit.vue
index ee1f15c189..38c8664575 100644
--- a/packages/frontend/src/components/MkOmit.vue
+++ b/packages/frontend/src/components/MkOmit.vue
@@ -62,11 +62,11 @@ onUnmounted(() => {
 			left: 0;
 			width: 100%;
 			height: 64px;
-			background: linear-gradient(0deg, var(--panel), color(from var(--panel) srgb r g b / 0));
+			background: linear-gradient(0deg, var(--MI_THEME-panel), color(from var(--MI_THEME-panel) srgb r g b / 0));
 
 			> .fadeLabel {
 				display: inline-block;
-				background: var(--panel);
+				background: var(--MI_THEME-panel);
 				padding: 6px 10px;
 				font-size: 0.8em;
 				border-radius: 999px;
@@ -75,7 +75,7 @@ onUnmounted(() => {
 
 			&:hover {
 				> .fadeLabel {
-					background: var(--panelHighlight);
+					background: var(--MI_THEME-panelHighlight);
 				}
 			}
 		}
diff --git a/packages/frontend/src/components/MkPagePreview.vue b/packages/frontend/src/components/MkPagePreview.vue
index 8559d4b96e..b5281d8a3d 100644
--- a/packages/frontend/src/components/MkPagePreview.vue
+++ b/packages/frontend/src/components/MkPagePreview.vue
@@ -54,7 +54,7 @@ const props = defineProps<{
 
 	&:hover {
 		text-decoration: none;
-		color: var(--accent);
+		color: var(--MI_THEME-accent);
 	}
 
 	&:focus-within {
@@ -69,7 +69,7 @@ const props = defineProps<{
 			height: 100%;
 			border-radius: var(--radius);
 			pointer-events: none;
-			box-shadow: inset 0 0 0 2px var(--focus);
+			box-shadow: inset 0 0 0 2px var(--MI_THEME-focus);
 		}
 	}
 
@@ -80,7 +80,7 @@ const props = defineProps<{
 	}
 
 	> article {
-		background-color: var(--panel);
+		background-color: var(--MI_THEME-panel);
 		padding: 16px;
 		border-radius: var(--radius);
 
diff --git a/packages/frontend/src/components/MkPageWindow.vue b/packages/frontend/src/components/MkPageWindow.vue
index 2b993ab12f..421051f73d 100644
--- a/packages/frontend/src/components/MkPageWindow.vue
+++ b/packages/frontend/src/components/MkPageWindow.vue
@@ -179,7 +179,7 @@ defineExpose({
 	overscroll-behavior: contain;
 
 	min-height: 100%;
-	background: var(--bg);
+	background: var(--MI_THEME-bg);
 
 	--margin: var(--marginHalf);
 }
diff --git a/packages/frontend/src/components/MkPoll.vue b/packages/frontend/src/components/MkPoll.vue
index e1d5db2730..48913004e0 100644
--- a/packages/frontend/src/components/MkPoll.vue
+++ b/packages/frontend/src/components/MkPoll.vue
@@ -9,7 +9,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 		<li v-for="(choice, i) in poll.choices" :key="i" :class="$style.choice" @click="vote(i)">
 			<div :class="$style.bg" :style="{ 'width': `${showResult ? (choice.votes / total * 100) : 0}%` }"></div>
 			<span :class="$style.fg">
-				<template v-if="choice.isVoted"><i class="ti ti-check" style="margin-right: 4px; color: var(--accent);"></i></template>
+				<template v-if="choice.isVoted"><i class="ti ti-check" style="margin-right: 4px; color: var(--MI_THEME-accent);"></i></template>
 				<Mfm :text="choice.text" :plain="true"/>
 				<span v-if="showResult" style="margin-left: 4px; opacity: 0.7;">({{ i18n.tsx._poll.votesCount({ n: choice.votes }) }})</span>
 			</span>
@@ -114,8 +114,8 @@ const vote = async (id) => {
 	position: relative;
 	margin: 4px 0;
 	padding: 4px;
-	//border: solid 0.5px var(--divider);
-	background: var(--accentedBg);
+	//border: solid 0.5px var(--MI_THEME-divider);
+	background: var(--MI_THEME-accentedBg);
 	border-radius: 4px;
 	overflow: clip;
 	cursor: pointer;
@@ -126,8 +126,8 @@ const vote = async (id) => {
 	top: 0;
 	left: 0;
 	height: 100%;
-	background: var(--accent);
-	background: linear-gradient(90deg,var(--buttonGradateA),var(--buttonGradateB));
+	background: var(--MI_THEME-accent);
+	background: linear-gradient(90deg,var(--MI_THEME-buttonGradateA),var(--MI_THEME-buttonGradateB));
 	transition: width 1s ease;
 }
 
@@ -135,12 +135,12 @@ const vote = async (id) => {
 	position: relative;
 	display: inline-block;
 	padding: 3px 5px;
-	background: var(--panel);
+	background: var(--MI_THEME-panel);
 	border-radius: 3px;
 }
 
 .info {
-	color: var(--fg);
+	color: var(--MI_THEME-fg);
 }
 
 .done {
diff --git a/packages/frontend/src/components/MkPostForm.vue b/packages/frontend/src/components/MkPostForm.vue
index 471ffdd896..76a6e4212a 100644
--- a/packages/frontend/src/components/MkPostForm.vue
+++ b/packages/frontend/src/components/MkPostForm.vue
@@ -1113,7 +1113,7 @@ defineExpose({
 		outline: none;
 
 		.submitInner {
-			outline: 2px solid var(--fgOnAccent);
+			outline: 2px solid var(--MI_THEME-fgOnAccent);
 			outline-offset: -4px;
 		}
 	}
@@ -1128,13 +1128,13 @@ defineExpose({
 
 	&:not(:disabled):hover {
 		> .inner {
-			background: linear-gradient(90deg, hsl(from var(--accent) h s calc(l + 5)), hsl(from var(--accent) h s calc(l + 5)));
+			background: linear-gradient(90deg, hsl(from var(--MI_THEME-accent) h s calc(l + 5)), hsl(from var(--MI_THEME-accent) h s calc(l + 5)));
 		}
 	}
 
 	&:not(:disabled):active {
 		> .inner {
-			background: linear-gradient(90deg, hsl(from var(--accent) h s calc(l + 5)), hsl(from var(--accent) h s calc(l + 5)));
+			background: linear-gradient(90deg, hsl(from var(--MI_THEME-accent) h s calc(l + 5)), hsl(from var(--MI_THEME-accent) h s calc(l + 5)));
 		}
 	}
 }
@@ -1156,8 +1156,8 @@ defineExpose({
 	border-radius: 6px;
 	min-width: 90px;
 	box-sizing: border-box;
-	color: var(--fgOnAccent);
-	background: linear-gradient(90deg, var(--buttonGradateA), var(--buttonGradateB));
+	color: var(--MI_THEME-fgOnAccent);
+	background: linear-gradient(90deg, var(--MI_THEME-buttonGradateA), var(--MI_THEME-buttonGradateB));
 }
 
 .headerRightItem {
@@ -1166,7 +1166,7 @@ defineExpose({
 	border-radius: 6px;
 
 	&:hover {
-		background: var(--X5);
+		background: var(--MI_THEME-X5);
 	}
 
 	&:disabled {
@@ -1218,7 +1218,7 @@ html[data-color-scheme=light] .preview {
 
 .withQuote {
 	margin: 0 0 8px 0;
-	color: var(--accent);
+	color: var(--MI_THEME-accent);
 }
 
 .toSpecified {
@@ -1238,7 +1238,7 @@ html[data-color-scheme=light] .preview {
 	margin-right: 14px;
 	padding: 8px 0 8px 8px;
 	border-radius: 8px;
-	background: var(--X4);
+	background: var(--MI_THEME-X4);
 }
 
 .hasNotSpecifiedMentions {
@@ -1257,7 +1257,7 @@ html[data-color-scheme=light] .preview {
 	border: none;
 	border-radius: 0;
 	background: transparent;
-	color: var(--fg);
+	color: var(--MI_THEME-fg);
 	font-family: inherit;
 
 	&:focus {
@@ -1272,14 +1272,14 @@ html[data-color-scheme=light] .preview {
 .cw {
 	z-index: 1;
 	padding-bottom: 8px;
-	border-bottom: solid 0.5px var(--divider);
+	border-bottom: solid 0.5px var(--MI_THEME-divider);
 }
 
 .hashtags {
 	z-index: 1;
 	padding-top: 8px;
 	padding-bottom: 8px;
-	border-top: solid 0.5px var(--divider);
+	border-top: solid 0.5px var(--MI_THEME-divider);
 }
 
 .textOuter {
@@ -1305,7 +1305,7 @@ html[data-color-scheme=light] .preview {
 	right: 2px;
 	padding: 4px 6px;
 	font-size: .9em;
-	color: var(--warn);
+	color: var(--MI_THEME-warn);
 	border-radius: 6px;
 	min-width: 1.6em;
 	text-align: center;
@@ -1349,16 +1349,16 @@ html[data-color-scheme=light] .preview {
 	border-radius: 6px;
 
 	&:hover {
-		background: var(--X5);
+		background: var(--MI_THEME-X5);
 	}
 
 	&.footerButtonActive {
-		color: var(--accent);
+		color: var(--MI_THEME-accent);
 	}
 }
 
 .previewButtonActive {
-	color: var(--accent);
+	color: var(--MI_THEME-accent);
 }
 
 @container (max-width: 500px) {
diff --git a/packages/frontend/src/components/MkPostFormAttaches.vue b/packages/frontend/src/components/MkPostFormAttaches.vue
index 42322fec3d..ee7038df64 100644
--- a/packages/frontend/src/components/MkPostFormAttaches.vue
+++ b/packages/frontend/src/components/MkPostFormAttaches.vue
@@ -216,7 +216,7 @@ function showFileMenu(file: Misskey.entities.DriveFile, ev: MouseEvent | Keyboar
 	width: 100%;
 	height: 100%;
 	z-index: 1;
-	color: var(--fg);
+	color: var(--MI_THEME-fg);
 }
 
 .sensitive {
diff --git a/packages/frontend/src/components/MkRadio.vue b/packages/frontend/src/components/MkRadio.vue
index 22fc86723e..e735d9fff8 100644
--- a/packages/frontend/src/components/MkRadio.vue
+++ b/packages/frontend/src/components/MkRadio.vue
@@ -53,9 +53,9 @@ function toggle(): void {
 	cursor: pointer;
 	padding: 7px 10px;
 	min-width: 60px;
-	background-color: var(--panel);
+	background-color: var(--MI_THEME-panel);
 	background-clip: padding-box !important;
-	border: solid 1px var(--panel);
+	border: solid 1px var(--MI_THEME-panel);
 	border-radius: 6px;
 	font-size: 90%;
 	transition: all 0.2s;
@@ -67,25 +67,25 @@ function toggle(): void {
 	}
 
 	&:hover {
-		border-color: var(--inputBorderHover) !important;
+		border-color: var(--MI_THEME-inputBorderHover) !important;
 	}
 
 	&:focus-within {
 		outline: none;
-		box-shadow: 0 0 0 2px var(--focus);
+		box-shadow: 0 0 0 2px var(--MI_THEME-focus);
 	}
 
 	&.checked {
-		background-color: var(--accentedBg) !important;
-		border-color: var(--accentedBg) !important;
-		color: var(--accent);
+		background-color: var(--MI_THEME-accentedBg) !important;
+		border-color: var(--MI_THEME-accentedBg) !important;
+		color: var(--MI_THEME-accent);
 		cursor: default !important;
 
 		> .button {
-			border-color: var(--accent);
+			border-color: var(--MI_THEME-accent);
 
 			&::after {
-				background-color: var(--accent);
+				background-color: var(--MI_THEME-accent);
 				transform: scale(1);
 				opacity: 1;
 			}
@@ -106,7 +106,7 @@ function toggle(): void {
 	width: 14px;
 	height: 14px;
 	background: none;
-	border: solid 2px var(--inputBorder);
+	border: solid 2px var(--MI_THEME-inputBorder);
 	border-radius: 100%;
 	transition: inherit;
 
diff --git a/packages/frontend/src/components/MkRadios.vue b/packages/frontend/src/components/MkRadios.vue
index 705c93f770..af81eb814d 100644
--- a/packages/frontend/src/components/MkRadios.vue
+++ b/packages/frontend/src/components/MkRadios.vue
@@ -77,7 +77,7 @@ export default defineComponent({
 	> .caption {
 		font-size: 0.85em;
 		padding: 8px 0 0 0;
-		color: var(--fgTransparentWeak);
+		color: var(--MI_THEME-fgTransparentWeak);
 
 		&:empty {
 			display: none;
diff --git a/packages/frontend/src/components/MkRange.vue b/packages/frontend/src/components/MkRange.vue
index cfaaa67d58..264b559222 100644
--- a/packages/frontend/src/components/MkRange.vue
+++ b/packages/frontend/src/components/MkRange.vue
@@ -212,7 +212,7 @@ function onMousedown(ev: MouseEvent | TouchEvent) {
 	> .caption {
 		font-size: 0.85em;
 		padding: 8px 0 0 0;
-		color: var(--fgTransparentWeak);
+		color: var(--MI_THEME-fgTransparentWeak);
 
 		&:empty {
 			display: none;
@@ -224,8 +224,8 @@ function onMousedown(ev: MouseEvent | TouchEvent) {
 
 	> .body {
 		padding: 7px 12px;
-		background: var(--panel);
-		border: solid 1px var(--panel);
+		background: var(--MI_THEME-panel);
+		border: solid 1px var(--MI_THEME-panel);
 		border-radius: 6px;
 
 		> .container {
@@ -250,7 +250,7 @@ function onMousedown(ev: MouseEvent | TouchEvent) {
 					top: 0;
 					left: 0;
 					height: 100%;
-					background: var(--accent);
+					background: var(--MI_THEME-accent);
 					opacity: 0.5;
 				}
 			}
@@ -272,7 +272,7 @@ function onMousedown(ev: MouseEvent | TouchEvent) {
 					width: $tickWidth;
 					height: 3px;
 					margin-left: - math.div($tickWidth, 2);
-					background: var(--divider);
+					background: var(--MI_THEME-divider);
 					border-radius: 999px;
 				}
 			}
@@ -282,11 +282,11 @@ function onMousedown(ev: MouseEvent | TouchEvent) {
 				width: $thumbWidth;
 				height: $thumbHeight;
 				cursor: grab;
-				background: var(--accent);
+				background: var(--MI_THEME-accent);
 				border-radius: 999px;
 
 				&:hover {
-					background: var(--accentLighten);
+					background: var(--MI_THEME-accentLighten);
 				}
 			}
 		}
diff --git a/packages/frontend/src/components/MkReactionEffect.vue b/packages/frontend/src/components/MkReactionEffect.vue
index 361e246e9f..5a59a5e055 100644
--- a/packages/frontend/src/components/MkReactionEffect.vue
+++ b/packages/frontend/src/components/MkReactionEffect.vue
@@ -60,7 +60,7 @@ onMounted(() => {
 	right: 0;
 	bottom: 0;
 	margin: auto;
-	color: var(--accent);
+	color: var(--MI_THEME-accent);
 	font-size: 18px;
 	font-weight: bold;
 	transform: translateY(-30px);
diff --git a/packages/frontend/src/components/MkReactionsViewer.details.vue b/packages/frontend/src/components/MkReactionsViewer.details.vue
index 8038ec7429..f4c3643ba8 100644
--- a/packages/frontend/src/components/MkReactionsViewer.details.vue
+++ b/packages/frontend/src/components/MkReactionsViewer.details.vue
@@ -57,7 +57,7 @@ function getReactionName(reaction: string): string {
 	max-width: 100px;
 	padding-right: 10px;
 	text-align: center;
-	border-right: solid 0.5px var(--divider);
+	border-right: solid 0.5px var(--MI_THEME-divider);
 }
 
 .reactionIcon {
diff --git a/packages/frontend/src/components/MkReactionsViewer.reaction.vue b/packages/frontend/src/components/MkReactionsViewer.reaction.vue
index f42a0b3227..b65038aadc 100644
--- a/packages/frontend/src/components/MkReactionsViewer.reaction.vue
+++ b/packages/frontend/src/components/MkReactionsViewer.reaction.vue
@@ -180,7 +180,7 @@ if (!mock) {
 	justify-content: center;
 
 	&.canToggle {
-		background: var(--buttonBg);
+		background: var(--MI_THEME-buttonBg);
 
 		&:hover {
 			background: rgba(0, 0, 0, 0.1);
@@ -214,12 +214,12 @@ if (!mock) {
 	}
 
 	&.reacted, &.reacted:hover {
-		background: var(--accentedBg);
-		color: var(--accent);
-		box-shadow: 0 0 0 1px var(--accent) inset;
+		background: var(--MI_THEME-accentedBg);
+		color: var(--MI_THEME-accent);
+		box-shadow: 0 0 0 1px var(--MI_THEME-accent) inset;
 
 		> .count {
-			color: var(--accent);
+			color: var(--MI_THEME-accent);
 		}
 
 		> .icon {
diff --git a/packages/frontend/src/components/MkRemoteCaution.vue b/packages/frontend/src/components/MkRemoteCaution.vue
index f1050d26e6..3ffb50dbd9 100644
--- a/packages/frontend/src/components/MkRemoteCaution.vue
+++ b/packages/frontend/src/components/MkRemoteCaution.vue
@@ -19,14 +19,14 @@ defineProps<{
 .root {
 	font-size: 0.8em;
 	padding: 16px;
-	background: var(--infoWarnBg);
-	color: var(--infoWarnFg);
+	background: var(--MI_THEME-infoWarnBg);
+	color: var(--MI_THEME-infoWarnFg);
 	border-radius: var(--radius);
 	overflow: clip;
 }
 
 .link {
 	margin-left: 4px;
-	color: var(--accent);
+	color: var(--MI_THEME-accent);
 }
 </style>
diff --git a/packages/frontend/src/components/MkRetentionLineChart.vue b/packages/frontend/src/components/MkRetentionLineChart.vue
index c3daa9c9a4..d41793b0fa 100644
--- a/packages/frontend/src/components/MkRetentionLineChart.vue
+++ b/packages/frontend/src/components/MkRetentionLineChart.vue
@@ -44,7 +44,7 @@ onMounted(async () => {
 
 	const vLineColor = defaultStore.state.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
 
-	const accent = tinycolor(getComputedStyle(document.documentElement).getPropertyValue('--accent'));
+	const accent = tinycolor(getComputedStyle(document.documentElement).getPropertyValue('--MI_THEME-accent'));
 	const color = accent.toHex();
 
 	if (chartEl.value == null) return;
diff --git a/packages/frontend/src/components/MkRippleEffect.vue b/packages/frontend/src/components/MkRippleEffect.vue
index ee5bb73ebf..2949cf156d 100644
--- a/packages/frontend/src/components/MkRippleEffect.vue
+++ b/packages/frontend/src/components/MkRippleEffect.vue
@@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 <template>
 <div :class="$style.root" :style="{ zIndex, top: `${y - 64}px`, left: `${x - 64}px` }">
 	<svg width="128" height="128" viewBox="0 0 128 128" xmlns="http://www.w3.org/2000/svg">
-		<circle fill="none" cx="64" cy="64" style="stroke: var(--accent);">
+		<circle fill="none" cx="64" cy="64" style="stroke: var(--MI_THEME-accent);">
 			<animate
 				attributeName="r"
 				begin="0s" dur="0.5s"
@@ -27,7 +27,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 			/>
 		</circle>
 		<g fill="none" fill-rule="evenodd">
-			<circle v-for="(particle, i) in particles" :key="i" :fill="particle.color" style="stroke: var(--accent);">
+			<circle v-for="(particle, i) in particles" :key="i" :fill="particle.color" style="stroke: var(--MI_THEME-accent);">
 				<animate
 					attributeName="r"
 					begin="0s" dur="0.8s"
diff --git a/packages/frontend/src/components/MkRolePreview.vue b/packages/frontend/src/components/MkRolePreview.vue
index ce17ae08e0..3f14c5b5e0 100644
--- a/packages/frontend/src/components/MkRolePreview.vue
+++ b/packages/frontend/src/components/MkRolePreview.vue
@@ -6,8 +6,8 @@ SPDX-License-Identifier: AGPL-3.0-only
 <template>
 <MkA :to="forModeration ? `/admin/roles/${role.id}` : `/roles/${role.id}`" :class="$style.root" tabindex="-1" :style="{ '--color': role.color }">
 	<template v-if="forModeration">
-		<i v-if="role.isPublic" class="ti ti-world" :class="$style.icon" style="color: var(--success)"></i>
-		<i v-else class="ti ti-lock" :class="$style.icon" style="color: var(--warn)"></i>
+		<i v-if="role.isPublic" class="ti ti-world" :class="$style.icon" style="color: var(--MI_THEME-success)"></i>
+		<i v-else class="ti ti-lock" :class="$style.icon" style="color: var(--MI_THEME-warn)"></i>
 	</template>
 
 	<div v-adaptive-bg class="_panel" :class="$style.body">
@@ -17,8 +17,8 @@ SPDX-License-Identifier: AGPL-3.0-only
 					<img :class="$style.bodyBadge" :src="role.iconUrl"/>
 				</template>
 				<template v-else>
-					<i v-if="role.isAdministrator" class="ti ti-crown" style="color: var(--accent);"></i>
-					<i v-else-if="role.isModerator" class="ti ti-shield" style="color: var(--accent);"></i>
+					<i v-if="role.isAdministrator" class="ti ti-crown" style="color: var(--MI_THEME-accent);"></i>
+					<i v-else-if="role.isModerator" class="ti ti-shield" style="color: var(--MI_THEME-accent);"></i>
 					<i v-else class="ti ti-user" style="opacity: 0.7;"></i>
 				</template>
 			</span>
diff --git a/packages/frontend/src/components/MkSelect.vue b/packages/frontend/src/components/MkSelect.vue
index 343524fc82..a2ec384ac5 100644
--- a/packages/frontend/src/components/MkSelect.vue
+++ b/packages/frontend/src/components/MkSelect.vue
@@ -202,7 +202,7 @@ function show() {
 .caption {
 	font-size: 0.85em;
 	padding: 8px 0 0 0;
-	color: var(--fgTransparentWeak);
+	color: var(--MI_THEME-fgTransparentWeak);
 
 	&:empty {
 		display: none;
@@ -220,8 +220,8 @@ function show() {
 
 	&.focused {
 		> .inputCore {
-			border-color: var(--accent) !important;
-			//box-shadow: 0 0 0 4px var(--focus);
+			border-color: var(--MI_THEME-accent) !important;
+			//box-shadow: 0 0 0 4px var(--MI_THEME-focus);
 		}
 	}
 
@@ -240,7 +240,7 @@ function show() {
 
 	&:hover {
 		> .inputCore {
-			border-color: var(--inputBorderHover) !important;
+			border-color: var(--MI_THEME-inputBorderHover) !important;
 		}
 	}
 }
@@ -256,9 +256,9 @@ function show() {
 	font: inherit;
 	font-weight: normal;
 	font-size: 1em;
-	color: var(--fg);
-	background: var(--panel);
-	border: solid 1px var(--panel);
+	color: var(--MI_THEME-fg);
+	background: var(--MI_THEME-panel);
+	border: solid 1px var(--MI_THEME-panel);
 	border-radius: 6px;
 	outline: none;
 	box-shadow: none;
diff --git a/packages/frontend/src/components/MkSignin.input.vue b/packages/frontend/src/components/MkSignin.input.vue
index 6336b78c80..34c22abc31 100644
--- a/packages/frontend/src/components/MkSignin.input.vue
+++ b/packages/frontend/src/components/MkSignin.input.vue
@@ -162,8 +162,8 @@ async function specifyHostAndOpenRemote(options: OpenOnRemoteOptions): Promise<v
 
 .avatar {
 	margin: 0 auto;
-	background-color: color-mix(in srgb, var(--fg), transparent 85%);
-	color: color-mix(in srgb, var(--fg), transparent 25%);
+	background-color: color-mix(in srgb, var(--MI_THEME-fg), transparent 85%);
+	color: color-mix(in srgb, var(--MI_THEME-fg), transparent 25%);
 	text-align: center;
 	height: 64px;
 	width: 64px;
@@ -188,7 +188,7 @@ async function specifyHostAndOpenRemote(options: OpenOnRemoteOptions): Promise<v
 	margin: .4em auto;
 	width: 100%;
 	height: 1px;
-	background: var(--divider);
+	background: var(--MI_THEME-divider);
 }
 
 .orMsg {
@@ -196,9 +196,9 @@ async function specifyHostAndOpenRemote(options: OpenOnRemoteOptions): Promise<v
 	top: -.6em;
 	display: inline-block;
 	padding: 0 1em;
-	background: var(--panel);
+	background: var(--MI_THEME-panel);
 	font-size: 0.8em;
-	color: var(--fgOnPanel);
+	color: var(--MI_THEME-fgOnPanel);
 	margin: 0;
 	left: 50%;
 	transform: translateX(-50%);
diff --git a/packages/frontend/src/components/MkSignin.passkey.vue b/packages/frontend/src/components/MkSignin.passkey.vue
index 0d68955fab..e5a56ab66d 100644
--- a/packages/frontend/src/components/MkSignin.passkey.vue
+++ b/packages/frontend/src/components/MkSignin.passkey.vue
@@ -75,8 +75,8 @@ onMounted(() => {
 
 .passkeyIcon {
 	margin: 0 auto;
-	background-color: var(--accentedBg);
-	color: var(--accent);
+	background-color: var(--MI_THEME-accentedBg);
+	color: var(--MI_THEME-accent);
 	text-align: center;
 	height: 64px;
 	width: 64px;
diff --git a/packages/frontend/src/components/MkSignin.password.vue b/packages/frontend/src/components/MkSignin.password.vue
index 2d79e2aeb1..f30bf5f861 100644
--- a/packages/frontend/src/components/MkSignin.password.vue
+++ b/packages/frontend/src/components/MkSignin.password.vue
@@ -163,7 +163,7 @@ defineExpose({
 	margin: .4em auto;
 	width: 100%;
 	height: 1px;
-	background: var(--divider);
+	background: var(--MI_THEME-divider);
 }
 
 .orMsg {
@@ -171,9 +171,9 @@ defineExpose({
 	top: -.6em;
 	display: inline-block;
 	padding: 0 1em;
-	background: var(--panel);
+	background: var(--MI_THEME-panel);
 	font-size: 0.8em;
-	color: var(--fgOnPanel);
+	color: var(--MI_THEME-fgOnPanel);
 	margin: 0;
 	left: 50%;
 	transform: translateX(-50%);
diff --git a/packages/frontend/src/components/MkSignin.totp.vue b/packages/frontend/src/components/MkSignin.totp.vue
index 880c08315e..670b8057c2 100644
--- a/packages/frontend/src/components/MkSignin.totp.vue
+++ b/packages/frontend/src/components/MkSignin.totp.vue
@@ -57,8 +57,8 @@ const isBackupCode = ref(false);
 
 .totpIcon {
 	margin: 0 auto;
-	background-color: var(--accentedBg);
-	color: var(--accent);
+	background-color: var(--MI_THEME-accentedBg);
+	color: var(--MI_THEME-accent);
 	text-align: center;
 	height: 64px;
 	width: 64px;
diff --git a/packages/frontend/src/components/MkSignin.vue b/packages/frontend/src/components/MkSignin.vue
index 26e1ac516c..a79d7cf07a 100644
--- a/packages/frontend/src/components/MkSignin.vue
+++ b/packages/frontend/src/components/MkSignin.vue
@@ -409,7 +409,7 @@ onBeforeUnmount(() => {
 	left: 0;
 	width: 100%;
 	height: 100%;
-	background-color: color-mix(in srgb, var(--panel), transparent 50%);
+	background-color: color-mix(in srgb, var(--MI_THEME-panel), transparent 50%);
 	display: flex;
 	justify-content: center;
 	align-items: center;
diff --git a/packages/frontend/src/components/MkSigninDialog.vue b/packages/frontend/src/components/MkSigninDialog.vue
index 8351d7d5e0..2aa11ac319 100644
--- a/packages/frontend/src/components/MkSigninDialog.vue
+++ b/packages/frontend/src/components/MkSigninDialog.vue
@@ -68,7 +68,7 @@ function onLogin(res) {
 	height: 100%;
 	max-height: 450px;
 	box-sizing: border-box;
-	background: var(--panel);
+	background: var(--MI_THEME-panel);
 	border-radius: var(--radius);
 }
 
@@ -83,7 +83,7 @@ function onLogin(res) {
 	align-items: center;
 	font-weight: bold;
 	backdrop-filter: var(--blur, blur(15px));
-	background: var(--acrylicBg);
+	background: var(--MI_THEME-acrylicBg);
 	z-index: 1;
 }
 
diff --git a/packages/frontend/src/components/MkSignupDialog.form.vue b/packages/frontend/src/components/MkSignupDialog.form.vue
index ff096dc729..a0c5488983 100644
--- a/packages/frontend/src/components/MkSignupDialog.form.vue
+++ b/packages/frontend/src/components/MkSignupDialog.form.vue
@@ -21,12 +21,12 @@ SPDX-License-Identifier: AGPL-3.0-only
 				<template #caption>
 					<div><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.cannotBeChangedLater }}</div>
 					<span v-if="usernameState === 'wait'" style="color:#999"><MkLoading :em="true"/> {{ i18n.ts.checking }}</span>
-					<span v-else-if="usernameState === 'ok'" style="color: var(--success)"><i class="ti ti-check ti-fw"></i> {{ i18n.ts.available }}</span>
-					<span v-else-if="usernameState === 'unavailable'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.unavailable }}</span>
-					<span v-else-if="usernameState === 'error'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.error }}</span>
-					<span v-else-if="usernameState === 'invalid-format'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.usernameInvalidFormat }}</span>
-					<span v-else-if="usernameState === 'min-range'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.tooShort }}</span>
-					<span v-else-if="usernameState === 'max-range'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.tooLong }}</span>
+					<span v-else-if="usernameState === 'ok'" style="color: var(--MI_THEME-success)"><i class="ti ti-check ti-fw"></i> {{ i18n.ts.available }}</span>
+					<span v-else-if="usernameState === 'unavailable'" style="color: var(--MI_THEME-error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.unavailable }}</span>
+					<span v-else-if="usernameState === 'error'" style="color: var(--MI_THEME-error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.error }}</span>
+					<span v-else-if="usernameState === 'invalid-format'" style="color: var(--MI_THEME-error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.usernameInvalidFormat }}</span>
+					<span v-else-if="usernameState === 'min-range'" style="color: var(--MI_THEME-error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.tooShort }}</span>
+					<span v-else-if="usernameState === 'max-range'" style="color: var(--MI_THEME-error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.tooLong }}</span>
 				</template>
 			</MkInput>
 			<MkInput v-if="instance.emailRequiredForSignup" v-model="email" :debounce="true" type="email" :spellcheck="false" required data-cy-signup-email @update:modelValue="onChangeEmail">
@@ -34,32 +34,32 @@ SPDX-License-Identifier: AGPL-3.0-only
 				<template #prefix><i class="ti ti-mail"></i></template>
 				<template #caption>
 					<span v-if="emailState === 'wait'" style="color:#999"><MkLoading :em="true"/> {{ i18n.ts.checking }}</span>
-					<span v-else-if="emailState === 'ok'" style="color: var(--success)"><i class="ti ti-check ti-fw"></i> {{ i18n.ts.available }}</span>
-					<span v-else-if="emailState === 'unavailable:used'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts._emailUnavailable.used }}</span>
-					<span v-else-if="emailState === 'unavailable:format'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts._emailUnavailable.format }}</span>
-					<span v-else-if="emailState === 'unavailable:disposable'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts._emailUnavailable.disposable }}</span>
-					<span v-else-if="emailState === 'unavailable:banned'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts._emailUnavailable.banned }}</span>
-					<span v-else-if="emailState === 'unavailable:mx'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts._emailUnavailable.mx }}</span>
-					<span v-else-if="emailState === 'unavailable:smtp'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts._emailUnavailable.smtp }}</span>
-					<span v-else-if="emailState === 'unavailable'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.unavailable }}</span>
-					<span v-else-if="emailState === 'error'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.error }}</span>
+					<span v-else-if="emailState === 'ok'" style="color: var(--MI_THEME-success)"><i class="ti ti-check ti-fw"></i> {{ i18n.ts.available }}</span>
+					<span v-else-if="emailState === 'unavailable:used'" style="color: var(--MI_THEME-error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts._emailUnavailable.used }}</span>
+					<span v-else-if="emailState === 'unavailable:format'" style="color: var(--MI_THEME-error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts._emailUnavailable.format }}</span>
+					<span v-else-if="emailState === 'unavailable:disposable'" style="color: var(--MI_THEME-error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts._emailUnavailable.disposable }}</span>
+					<span v-else-if="emailState === 'unavailable:banned'" style="color: var(--MI_THEME-error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts._emailUnavailable.banned }}</span>
+					<span v-else-if="emailState === 'unavailable:mx'" style="color: var(--MI_THEME-error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts._emailUnavailable.mx }}</span>
+					<span v-else-if="emailState === 'unavailable:smtp'" style="color: var(--MI_THEME-error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts._emailUnavailable.smtp }}</span>
+					<span v-else-if="emailState === 'unavailable'" style="color: var(--MI_THEME-error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.unavailable }}</span>
+					<span v-else-if="emailState === 'error'" style="color: var(--MI_THEME-error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.error }}</span>
 				</template>
 			</MkInput>
 			<MkInput v-model="password" type="password" autocomplete="new-password" required data-cy-signup-password @update:modelValue="onChangePassword">
 				<template #label>{{ i18n.ts.password }}</template>
 				<template #prefix><i class="ti ti-lock"></i></template>
 				<template #caption>
-					<span v-if="passwordStrength == 'low'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.weakPassword }}</span>
-					<span v-if="passwordStrength == 'medium'" style="color: var(--warn)"><i class="ti ti-check ti-fw"></i> {{ i18n.ts.normalPassword }}</span>
-					<span v-if="passwordStrength == 'high'" style="color: var(--success)"><i class="ti ti-check ti-fw"></i> {{ i18n.ts.strongPassword }}</span>
+					<span v-if="passwordStrength == 'low'" style="color: var(--MI_THEME-error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.weakPassword }}</span>
+					<span v-if="passwordStrength == 'medium'" style="color: var(--MI_THEME-warn)"><i class="ti ti-check ti-fw"></i> {{ i18n.ts.normalPassword }}</span>
+					<span v-if="passwordStrength == 'high'" style="color: var(--MI_THEME-success)"><i class="ti ti-check ti-fw"></i> {{ i18n.ts.strongPassword }}</span>
 				</template>
 			</MkInput>
 			<MkInput v-model="retypedPassword" type="password" autocomplete="new-password" required data-cy-signup-password-retype @update:modelValue="onChangePasswordRetype">
 				<template #label>{{ i18n.ts.password }} ({{ i18n.ts.retype }})</template>
 				<template #prefix><i class="ti ti-lock"></i></template>
 				<template #caption>
-					<span v-if="passwordRetypeState == 'match'" style="color: var(--success)"><i class="ti ti-check ti-fw"></i> {{ i18n.ts.passwordMatched }}</span>
-					<span v-if="passwordRetypeState == 'not-match'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.passwordNotMatched }}</span>
+					<span v-if="passwordRetypeState == 'match'" style="color: var(--MI_THEME-success)"><i class="ti ti-check ti-fw"></i> {{ i18n.ts.passwordMatched }}</span>
+					<span v-if="passwordRetypeState == 'not-match'" style="color: var(--MI_THEME-error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.passwordNotMatched }}</span>
 				</template>
 			</MkInput>
 			<MkCaptcha v-if="instance.enableHcaptcha" ref="hcaptcha" v-model="hCaptchaResponse" :class="$style.captcha" provider="hcaptcha" :sitekey="instance.hcaptchaSiteKey"/>
@@ -304,8 +304,8 @@ async function onSubmit(): Promise<void> {
 	padding: 16px;
 	text-align: center;
 	font-size: 26px;
-	background-color: var(--accentedBg);
-	color: var(--accent);
+	background-color: var(--MI_THEME-accentedBg);
+	color: var(--MI_THEME-accent);
 }
 
 .captcha {
diff --git a/packages/frontend/src/components/MkSignupDialog.rules.vue b/packages/frontend/src/components/MkSignupDialog.rules.vue
index 59a3651cd4..1470f1e57e 100644
--- a/packages/frontend/src/components/MkSignupDialog.rules.vue
+++ b/packages/frontend/src/components/MkSignupDialog.rules.vue
@@ -21,7 +21,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 			<MkFolder v-if="availableServerRules" :defaultOpen="true">
 				<template #label>{{ i18n.ts.serverRules }}</template>
-				<template #suffix><i v-if="agreeServerRules" class="ti ti-check" style="color: var(--success)"></i></template>
+				<template #suffix><i v-if="agreeServerRules" class="ti ti-check" style="color: var(--MI_THEME-success)"></i></template>
 
 				<ol class="_gaps_s" :class="$style.rules">
 					<li v-for="item in instance.serverRules" :class="$style.rule"><div :class="$style.ruleText" v-html="item"></div></li>
@@ -32,7 +32,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 			<MkFolder v-if="availableTos || availablePrivacyPolicy" :defaultOpen="true">
 				<template #label>{{ tosPrivacyPolicyLabel }}</template>
-				<template #suffix><i v-if="agreeTosAndPrivacyPolicy" class="ti ti-check" style="color: var(--success)"></i></template>
+				<template #suffix><i v-if="agreeTosAndPrivacyPolicy" class="ti ti-check" style="color: var(--MI_THEME-success)"></i></template>
 				<div class="_gaps_s">
 					<div v-if="availableTos"><a :href="instance.tosUrl ?? undefined" class="_link" target="_blank">{{ i18n.ts.termsOfService }} <i class="ti ti-external-link"></i></a></div>
 					<div v-if="availablePrivacyPolicy"><a :href="instance.privacyPolicyUrl ?? undefined" class="_link" target="_blank">{{ i18n.ts.privacyPolicy }} <i class="ti ti-external-link"></i></a></div>
@@ -43,7 +43,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 			<MkFolder :defaultOpen="true">
 				<template #label>{{ i18n.ts.basicNotesBeforeCreateAccount }}</template>
-				<template #suffix><i v-if="agreeNote" class="ti ti-check" style="color: var(--success)"></i></template>
+				<template #suffix><i v-if="agreeNote" class="ti ti-check" style="color: var(--MI_THEME-success)"></i></template>
 
 				<a href="https://misskey-hub.net/docs/for-users/onboarding/warning/" class="_link" target="_blank">{{ i18n.ts.basicNotesBeforeCreateAccount }} <i class="ti ti-external-link"></i></a>
 
@@ -150,8 +150,8 @@ async function updateAgreeNote(v: boolean) {
 	padding: 16px;
 	text-align: center;
 	font-size: 26px;
-	background-color: var(--accentedBg);
-	color: var(--accent);
+	background-color: var(--MI_THEME-accentedBg);
+	color: var(--MI_THEME-accent);
 }
 
 .rules {
@@ -176,8 +176,8 @@ async function updateAgreeNote(v: boolean) {
 		width: 32px;
 		height: 32px;
 		line-height: 32px;
-		background-color: var(--accentedBg);
-		color: var(--accent);
+		background-color: var(--MI_THEME-accentedBg);
+		color: var(--MI_THEME-accent);
 		font-size: 13px;
 		font-weight: bold;
 		align-items: center;
diff --git a/packages/frontend/src/components/MkSourceCodeAvailablePopup.vue b/packages/frontend/src/components/MkSourceCodeAvailablePopup.vue
index 1845b01b69..438dd7e3a5 100644
--- a/packages/frontend/src/components/MkSourceCodeAvailablePopup.vue
+++ b/packages/frontend/src/components/MkSourceCodeAvailablePopup.vue
@@ -77,7 +77,7 @@ function close() {
 	text-align: center;
 	padding-top: 25px;
 	width: 100px;
-	color: var(--accent);
+	color: var(--MI_THEME-accent);
 }
 @media (max-width: 500px) {
 	.icon {
diff --git a/packages/frontend/src/components/MkSubNoteContent.vue b/packages/frontend/src/components/MkSubNoteContent.vue
index 3bbb163f0f..a36765b73c 100644
--- a/packages/frontend/src/components/MkSubNoteContent.vue
+++ b/packages/frontend/src/components/MkSubNoteContent.vue
@@ -62,11 +62,11 @@ const collapsed = ref(isLong);
 			left: 0;
 			width: 100%;
 			height: 64px;
-			background: linear-gradient(0deg, var(--panel), color(from var(--panel) srgb r g b / 0));
+			background: linear-gradient(0deg, var(--MI_THEME-panel), color(from var(--MI_THEME-panel) srgb r g b / 0));
 
 			> .fadeLabel {
 				display: inline-block;
-				background: var(--panel);
+				background: var(--MI_THEME-panel);
 				padding: 6px 10px;
 				font-size: 0.8em;
 				border-radius: 999px;
@@ -75,7 +75,7 @@ const collapsed = ref(isLong);
 
 			&:hover {
 				> .fadeLabel {
-					background: var(--panelHighlight);
+					background: var(--MI_THEME-panelHighlight);
 				}
 			}
 		}
@@ -84,13 +84,13 @@ const collapsed = ref(isLong);
 
 .reply {
 	margin-right: 6px;
-	color: var(--accent);
+	color: var(--MI_THEME-accent);
 }
 
 .rp {
 	margin-left: 4px;
 	font-style: oblique;
-	color: var(--renote);
+	color: var(--MI_THEME-renote);
 }
 
 .showLess {
@@ -102,7 +102,7 @@ const collapsed = ref(isLong);
 
 .showLessLabel {
 	display: inline-block;
-	background: var(--popup);
+	background: var(--MI_THEME-popup);
 	padding: 6px 10px;
 	font-size: 0.8em;
 	border-radius: 999px;
diff --git a/packages/frontend/src/components/MkSuperMenu.vue b/packages/frontend/src/components/MkSuperMenu.vue
index 3746ffd8f3..6e7a875dec 100644
--- a/packages/frontend/src/components/MkSuperMenu.vue
+++ b/packages/frontend/src/components/MkSuperMenu.vue
@@ -43,7 +43,7 @@ defineProps<{
 		& + .group {
 			margin-top: 16px;
 			padding-top: 16px;
-			border-top: solid 0.5px var(--divider);
+			border-top: solid 0.5px var(--MI_THEME-divider);
 		}
 
 		> .title {
@@ -64,7 +64,7 @@ defineProps<{
 
 				&:hover {
 					text-decoration: none;
-					background: var(--panelHighlight);
+					background: var(--MI_THEME-panelHighlight);
 				}
 
 				&:focus-visible {
@@ -72,12 +72,12 @@ defineProps<{
 				}
 
 				&.active {
-					color: var(--accent);
-					background: var(--accentedBg);
+					color: var(--MI_THEME-accent);
+					background: var(--MI_THEME-accentedBg);
 				}
 
 				&.danger {
-					color: var(--error);
+					color: var(--MI_THEME-error);
 				}
 
 				> .icon {
@@ -128,10 +128,10 @@ defineProps<{
 					&:hover {
 						text-decoration: none;
 						background: none;
-						color: var(--accent);
+						color: var(--MI_THEME-accent);
 
 						> .icon {
-							background: var(--accentedBg);
+							background: var(--MI_THEME-accentedBg);
 						}
 					}
 
@@ -144,7 +144,7 @@ defineProps<{
 						width: 60px;
 						height: 60px;
 						aspect-ratio: 1;
-						background: var(--panel);
+						background: var(--MI_THEME-panel);
 						border-radius: 100%;
 					}
 
diff --git a/packages/frontend/src/components/MkSwitch.button.vue b/packages/frontend/src/components/MkSwitch.button.vue
index 226908e221..fd8ed0992e 100644
--- a/packages/frontend/src/components/MkSwitch.button.vue
+++ b/packages/frontend/src/components/MkSwitch.button.vue
@@ -51,9 +51,9 @@ const toggle = () => {
 	width: calc(var(--height) * 1.6);
 	height: calc(var(--height) + 2px); // 枠線
 	outline: none;
-	background: var(--switchOffBg);
+	background: var(--MI_THEME-switchOffBg);
 	background-clip: content-box;
-	border: solid 1px var(--switchOffBg);
+	border: solid 1px var(--MI_THEME-switchOffBg);
 	border-radius: 999px;
 	cursor: pointer;
 	transition: inherit;
@@ -61,8 +61,8 @@ const toggle = () => {
 }
 
 .buttonChecked {
-	background-color: var(--switchOnBg) !important;
-	border-color: var(--switchOnBg) !important;
+	background-color: var(--MI_THEME-switchOnBg) !important;
+	border-color: var(--MI_THEME-switchOnBg) !important;
 }
 
 .buttonDisabled {
@@ -80,12 +80,12 @@ const toggle = () => {
 
 	&:not(.knobChecked) {
 		left: 3px;
-		background: var(--switchOffFg);
+		background: var(--MI_THEME-switchOffFg);
 	}
 }
 
 .knobChecked {
 	left: calc(calc(100% - var(--height)) + 3px);
-	background: var(--switchOnFg);
+	background: var(--MI_THEME-switchOnFg);
 }
 </style>
diff --git a/packages/frontend/src/components/MkSwitch.vue b/packages/frontend/src/components/MkSwitch.vue
index a0994d9cc9..5e6029ee40 100644
--- a/packages/frontend/src/components/MkSwitch.vue
+++ b/packages/frontend/src/components/MkSwitch.vue
@@ -59,7 +59,7 @@ const toggle = () => {
 
 	&:hover {
 		> .button {
-			border-color: var(--inputBorderHover) !important;
+			border-color: var(--MI_THEME-inputBorderHover) !important;
 		}
 	}
 
@@ -77,7 +77,7 @@ const toggle = () => {
 	margin: 0;
 
 	&:focus-visible ~ .toggle {
-		outline: 2px solid var(--focus);
+		outline: 2px solid var(--MI_THEME-focus);
 		outline-offset: 2px;
 	}
 }
@@ -87,7 +87,7 @@ const toggle = () => {
 	margin-top: 2px;
 	display: block;
 	transition: inherit;
-	color: var(--fg);
+	color: var(--MI_THEME-fg);
 }
 
 .label {
@@ -99,7 +99,7 @@ const toggle = () => {
 
 .caption {
 	margin: 8px 0 0 0;
-	color: var(--fgTransparentWeak);
+	color: var(--MI_THEME-fgTransparentWeak);
 	font-size: 0.85em;
 
 	&:empty {
diff --git a/packages/frontend/src/components/MkSystemWebhookEditor.vue b/packages/frontend/src/components/MkSystemWebhookEditor.vue
index ec3b1c90ca..23130d7f9f 100644
--- a/packages/frontend/src/components/MkSystemWebhookEditor.vue
+++ b/packages/frontend/src/components/MkSystemWebhookEditor.vue
@@ -261,8 +261,8 @@ onMounted(async () => {
 	bottom: 0;
 	left: 0;
 	padding: 12px;
-	border-top: solid 0.5px var(--divider);
-	background: var(--acrylicBg);
+	border-top: solid 0.5px var(--MI_THEME-divider);
+	background: var(--MI_THEME-acrylicBg);
 	-webkit-backdrop-filter: var(--blur, blur(15px));
 	backdrop-filter: var(--blur, blur(15px));
 }
@@ -289,6 +289,6 @@ onMounted(async () => {
 .description {
 	font-size: 0.85em;
 	padding: 8px 0 0 0;
-	color: var(--fgTransparentWeak);
+	color: var(--MI_THEME-fgTransparentWeak);
 }
 </style>
diff --git a/packages/frontend/src/components/MkTab.vue b/packages/frontend/src/components/MkTab.vue
index f2d0c95013..f557ffa5dc 100644
--- a/packages/frontend/src/components/MkTab.vue
+++ b/packages/frontend/src/components/MkTab.vue
@@ -47,13 +47,13 @@ export default defineComponent({
 		}
 
 		&.active {
-			color: var(--accent);
-			background: var(--accentedBg);
+			color: var(--MI_THEME-accent);
+			background: var(--MI_THEME-accentedBg);
 		}
 
 		&:not(.active):hover {
-			color: var(--fgHighlighted);
-			background: var(--panelHighlight);
+			color: var(--MI_THEME-fgHighlighted);
+			background: var(--MI_THEME-panelHighlight);
 		}
 
 		&:not(:first-child) {
diff --git a/packages/frontend/src/components/MkTagCloud.vue b/packages/frontend/src/components/MkTagCloud.vue
index 6b9c181597..87aa046963 100644
--- a/packages/frontend/src/components/MkTagCloud.vue
+++ b/packages/frontend/src/components/MkTagCloud.vue
@@ -33,7 +33,7 @@ watch(available, () => {
 	try {
 		window.TagCanvas.Start(idForCanvas, idForTags, {
 			textColour: '#ffffff',
-			outlineColour: tinycolor(computedStyle.getPropertyValue('--accent')).toHexString(),
+			outlineColour: tinycolor(computedStyle.getPropertyValue('--MI_THEME-accent')).toHexString(),
 			outlineRadius: 10,
 			initial: [-0.030, -0.010],
 			frontSelect: true,
diff --git a/packages/frontend/src/components/MkTextarea.vue b/packages/frontend/src/components/MkTextarea.vue
index 59490c552a..0139712232 100644
--- a/packages/frontend/src/components/MkTextarea.vue
+++ b/packages/frontend/src/components/MkTextarea.vue
@@ -159,7 +159,7 @@ onUnmounted(() => {
 .caption {
 	font-size: 0.85em;
 	padding: 8px 0 0 0;
-	color: var(--fgTransparentWeak);
+	color: var(--MI_THEME-fgTransparentWeak);
 
 	&:empty {
 		display: none;
@@ -179,9 +179,9 @@ onUnmounted(() => {
 	font: inherit;
 	font-weight: normal;
 	font-size: 1em;
-	color: var(--fg);
-	background: var(--panel);
-	border: solid 1px var(--panel);
+	color: var(--MI_THEME-fg);
+	background: var(--MI_THEME-panel);
+	border: solid 1px var(--MI_THEME-panel);
 	border-radius: 6px;
 	outline: none;
 	box-shadow: none;
@@ -189,13 +189,13 @@ onUnmounted(() => {
 	transition: border-color 0.1s ease-out;
 
 	&:hover {
-		border-color: var(--inputBorderHover) !important;
+		border-color: var(--MI_THEME-inputBorderHover) !important;
 	}
 }
 
 .focused {
 	> .textarea {
-		border-color: var(--accent) !important;
+		border-color: var(--MI_THEME-accent) !important;
 	}
 }
 
diff --git a/packages/frontend/src/components/MkTokenGenerateWindow.vue b/packages/frontend/src/components/MkTokenGenerateWindow.vue
index b32066c950..63dc93ae27 100644
--- a/packages/frontend/src/components/MkTokenGenerateWindow.vue
+++ b/packages/frontend/src/components/MkTokenGenerateWindow.vue
@@ -136,7 +136,7 @@ function enableAll(): void {
 .adminPermissions {
 	margin: 8px -6px 0;
 	padding: 24px 6px 6px;
-	border: 2px solid var(--error);
+	border: 2px solid var(--MI_THEME-error);
 	border-radius: calc(var(--radius) / 2);
 }
 
@@ -144,7 +144,7 @@ function enableAll(): void {
 	margin: -34px 0 6px 12px;
 	padding: 0 4px;
 	width: fit-content;
-	color: var(--error);
-	background: var(--panel);
+	color: var(--MI_THEME-error);
+	background: var(--MI_THEME-panel);
 }
 </style>
diff --git a/packages/frontend/src/components/MkTooltip.vue b/packages/frontend/src/components/MkTooltip.vue
index a3620aab68..10365d29b1 100644
--- a/packages/frontend/src/components/MkTooltip.vue
+++ b/packages/frontend/src/components/MkTooltip.vue
@@ -110,7 +110,7 @@ onUnmounted(() => {
 	box-sizing: border-box;
 	text-align: center;
 	border-radius: 4px;
-	border: solid 0.5px var(--divider);
+	border: solid 0.5px var(--MI_THEME-divider);
 	pointer-events: none;
 	transform-origin: center center;
 }
diff --git a/packages/frontend/src/components/MkTutorialDialog.Note.vue b/packages/frontend/src/components/MkTutorialDialog.Note.vue
index 2a26d22dc2..5644907434 100644
--- a/packages/frontend/src/components/MkTutorialDialog.Note.vue
+++ b/packages/frontend/src/components/MkTutorialDialog.Note.vue
@@ -18,7 +18,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 	<div style="text-align: center; padding: 0 16px;">{{ i18n.ts._initialTutorial._reaction.description }}</div>
 	<div>{{ i18n.ts._initialTutorial._reaction.letsTryReacting }}</div>
 	<MkNote :class="$style.exampleNoteRoot" :note="exampleNote" :mock="true" @reaction="addReaction" @removeReaction="removeReaction"/>
-	<div v-if="onceReacted"><b style="color: var(--accent);"><i class="ti ti-check"></i> {{ i18n.ts._initialTutorial.wellDone }}</b> {{ i18n.ts._initialTutorial._reaction.reactNotification }}<br>{{ i18n.ts._initialTutorial._reaction.reactDone }}</div>
+	<div v-if="onceReacted"><b style="color: var(--MI_THEME-accent);"><i class="ti ti-check"></i> {{ i18n.ts._initialTutorial.wellDone }}</b> {{ i18n.ts._initialTutorial._reaction.reactNotification }}<br>{{ i18n.ts._initialTutorial._reaction.reactDone }}</div>
 </div>
 </template>
 
@@ -106,12 +106,12 @@ function removeReaction(emoji) {
 <style lang="scss" module>
 .exampleNoteRoot {
 	border-radius: var(--radius);
-	border: var(--panelBorder);
-	background: var(--panel);
+	border: var(--MI_THEME-panelBorder);
+	background: var(--MI_THEME-panel);
 }
 
 .divider {
 	height: 1px;
-	background: var(--divider);
+	background: var(--MI_THEME-divider);
 }
 </style>
diff --git a/packages/frontend/src/components/MkTutorialDialog.PostNote.vue b/packages/frontend/src/components/MkTutorialDialog.PostNote.vue
index 27483cc7c2..7044e05804 100644
--- a/packages/frontend/src/components/MkTutorialDialog.PostNote.vue
+++ b/packages/frontend/src/components/MkTutorialDialog.PostNote.vue
@@ -82,13 +82,13 @@ const exampleCWNote = reactive<Misskey.entities.Note>({
 .exampleRoot {
 	max-width: none!important;
 	border-radius: var(--radius);
-	border: var(--panelBorder);
-	background: var(--panel);
+	border: var(--MI_THEME-panelBorder);
+	background: var(--MI_THEME-panel);
 }
 
 .divider {
 	height: 1px;
-	background: var(--divider);
+	background: var(--MI_THEME-divider);
 }
 
 .image {
@@ -101,7 +101,7 @@ const exampleCWNote = reactive<Misskey.entities.Note>({
 	display: block;
 	width: 100%;
 	height: 40px;
-	color: var(--fgOnAccent);
+	color: var(--MI_THEME-fgOnAccent);
 	font-weight: bold;
 	text-align: left;
 
@@ -117,7 +117,7 @@ const exampleCWNote = reactive<Misskey.entities.Note>({
 		right: 0;
 		bottom: 0;
 		border-radius: 999px;
-		background: linear-gradient(90deg, var(--buttonGradateA), var(--buttonGradateB));
+		background: linear-gradient(90deg, var(--MI_THEME-buttonGradateA), var(--MI_THEME-buttonGradateB));
 	}
 
 }
diff --git a/packages/frontend/src/components/MkTutorialDialog.Sensitive.vue b/packages/frontend/src/components/MkTutorialDialog.Sensitive.vue
index d8d4b5aab7..ce06b97b6b 100644
--- a/packages/frontend/src/components/MkTutorialDialog.Sensitive.vue
+++ b/packages/frontend/src/components/MkTutorialDialog.Sensitive.vue
@@ -15,7 +15,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 		:initialNote="exampleNote"
 		@fileChangeSensitive="doSucceeded"
 	></MkPostForm>
-	<div v-if="onceSucceeded"><b style="color: var(--accent);"><i class="ti ti-check"></i> {{ i18n.ts._initialTutorial.wellDone }}</b> {{ i18n.ts._initialTutorial._howToMakeAttachmentsSensitive.sensitiveSucceeded }}</div>
+	<div v-if="onceSucceeded"><b style="color: var(--MI_THEME-accent);"><i class="ti ti-check"></i> {{ i18n.ts._initialTutorial.wellDone }}</b> {{ i18n.ts._initialTutorial._howToMakeAttachmentsSensitive.sensitiveSucceeded }}</div>
 	<MkFolder>
 		<template #label>{{ i18n.ts.previewNoteText }}</template>
 		<MkNote :mock="true" :note="exampleNote" :class="$style.exampleRoot"></MkNote>
@@ -92,13 +92,13 @@ const exampleNote = reactive<Misskey.entities.Note>({
 <style lang="scss" module>
 .exampleRoot {
 	border-radius: var(--radius);
-	border: var(--panelBorder);
-	background: var(--panel);
+	border: var(--MI_THEME-panelBorder);
+	background: var(--MI_THEME-panel);
 }
 
 .divider {
 	height: 1px;
-	background: var(--divider);
+	background: var(--MI_THEME-divider);
 }
 
 .image {
@@ -111,7 +111,7 @@ const exampleNote = reactive<Misskey.entities.Note>({
 	display: block;
 	width: 100%;
 	height: 40px;
-	color: var(--fgOnAccent);
+	color: var(--MI_THEME-fgOnAccent);
 	font-weight: bold;
 	text-align: left;
 
@@ -127,7 +127,7 @@ const exampleNote = reactive<Misskey.entities.Note>({
 		right: 0;
 		bottom: 0;
 		border-radius: 999px;
-		background: linear-gradient(90deg, var(--buttonGradateA), var(--buttonGradateB));
+		background: linear-gradient(90deg, var(--MI_THEME-buttonGradateA), var(--MI_THEME-buttonGradateB));
 	}
 
 }
diff --git a/packages/frontend/src/components/MkTutorialDialog.Timeline.vue b/packages/frontend/src/components/MkTutorialDialog.Timeline.vue
index 2d4da3fbd4..9e33afbb53 100644
--- a/packages/frontend/src/components/MkTutorialDialog.Timeline.vue
+++ b/packages/frontend/src/components/MkTutorialDialog.Timeline.vue
@@ -32,13 +32,13 @@ import { basicTimelineIconClass, basicTimelineTypes } from '@/timelines.js';
 <style lang="scss" module>
 .exampleNoteRoot {
 	border-radius: var(--radius);
-	border: var(--panelBorder);
-	background: var(--panel);
+	border: var(--MI_THEME-panelBorder);
+	background: var(--MI_THEME-panel);
 }
 
 .divider {
 	height: 1px;
-	background: var(--divider);
+	background: var(--MI_THEME-divider);
 }
 
 .image {
@@ -51,7 +51,7 @@ import { basicTimelineIconClass, basicTimelineTypes } from '@/timelines.js';
 	display: block;
 	width: 100%;
 	height: 40px;
-	color: var(--fgOnAccent);
+	color: var(--MI_THEME-fgOnAccent);
 	font-weight: bold;
 	text-align: left;
 
@@ -67,7 +67,7 @@ import { basicTimelineIconClass, basicTimelineTypes } from '@/timelines.js';
 		right: 0;
 		bottom: 0;
 		border-radius: 999px;
-		background: linear-gradient(90deg, var(--buttonGradateA), var(--buttonGradateB));
+		background: linear-gradient(90deg, var(--MI_THEME-buttonGradateA), var(--MI_THEME-buttonGradateB));
 	}
 
 }
diff --git a/packages/frontend/src/components/MkTutorialDialog.vue b/packages/frontend/src/components/MkTutorialDialog.vue
index 1f5a2b9381..11d7c8dc4d 100644
--- a/packages/frontend/src/components/MkTutorialDialog.vue
+++ b/packages/frontend/src/components/MkTutorialDialog.vue
@@ -31,7 +31,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 					<MkAnimBg style="position: absolute; top: 0;" :scale="1.5"/>
 					<MkSpacer :marginMin="20" :marginMax="28">
 						<div class="_gaps" style="text-align: center;">
-							<i class="ti ti-confetti" style="display: block; margin: auto; font-size: 3em; color: var(--accent);"></i>
+							<i class="ti ti-confetti" style="display: block; margin: auto; font-size: 3em; color: var(--MI_THEME-accent);"></i>
 							<div style="font-size: 120%;">{{ i18n.ts._initialTutorial._landing.title }}</div>
 							<div>{{ i18n.ts._initialTutorial._landing.description }}</div>
 							<MkButton primary rounded gradate style="margin: 16px auto 0 auto;" @click="page++">{{ i18n.ts._initialTutorial.launchTutorial }} <i class="ti ti-arrow-right"></i></MkButton>
@@ -126,7 +126,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 					<MkAnimBg style="position: absolute; top: 0;" :scale="1.5"/>
 					<MkSpacer :marginMin="20" :marginMax="28">
 						<div class="_gaps" style="text-align: center;">
-							<i class="ti ti-check" style="display: block; margin: auto; font-size: 3em; color: var(--accent);"></i>
+							<i class="ti ti-check" style="display: block; margin: auto; font-size: 3em; color: var(--MI_THEME-accent);"></i>
 							<div style="font-size: 120%;">{{ i18n.ts._initialTutorial._done.title }}</div>
 							<I18n :src="i18n.ts._initialTutorial._done.description" tag="div" style="padding: 0 16px;">
 								<template #link>
@@ -223,7 +223,7 @@ async function close(skip: boolean) {
 
 .progressBarValue {
 	height: 100%;
-	background: linear-gradient(90deg, var(--buttonGradateA), var(--buttonGradateB));
+	background: linear-gradient(90deg, var(--MI_THEME-buttonGradateA), var(--MI_THEME-buttonGradateB));
 	transition: all 0.5s cubic-bezier(0,.5,.5,1);
 }
 
@@ -253,7 +253,7 @@ async function close(skip: boolean) {
 	left: 0;
 	flex-shrink: 0;
 	padding: 12px;
-	border-top: solid 0.5px var(--divider);
+	border-top: solid 0.5px var(--MI_THEME-divider);
 	-webkit-backdrop-filter: blur(15px);
 	backdrop-filter: blur(15px);
 }
diff --git a/packages/frontend/src/components/MkUpdated.vue b/packages/frontend/src/components/MkUpdated.vue
index f8af276836..fe50ab8cff 100644
--- a/packages/frontend/src/components/MkUpdated.vue
+++ b/packages/frontend/src/components/MkUpdated.vue
@@ -46,7 +46,7 @@ onMounted(() => {
 	max-width: 480px;
 	box-sizing: border-box;
 	text-align: center;
-	background: var(--panel);
+	background: var(--MI_THEME-panel);
 	border-radius: var(--radius);
 }
 
diff --git a/packages/frontend/src/components/MkUrlPreview.vue b/packages/frontend/src/components/MkUrlPreview.vue
index f5f9b43197..f38e31c894 100644
--- a/packages/frontend/src/components/MkUrlPreview.vue
+++ b/packages/frontend/src/components/MkUrlPreview.vue
@@ -219,7 +219,7 @@ onUnmounted(() => {
 	height: 1.5em;
 	padding: 0;
 	margin: 0;
-	color: var(--fg);
+	color: var(--MI_THEME-fg);
 	background: rgba(128, 128, 128, 0.2);
 	opacity: 0.7;
 
@@ -240,7 +240,7 @@ onUnmounted(() => {
 	position: relative;
 	display: block;
 	font-size: 14px;
-	box-shadow: 0 0 0 1px var(--divider);
+	box-shadow: 0 0 0 1px var(--MI_THEME-divider);
 	border-radius: 8px;
 	overflow: clip;
 
@@ -270,7 +270,7 @@ onUnmounted(() => {
 	height: 100%;
 	background-position: center;
 	background-size: cover;
-	background-color: var(--bg);
+	background-color: var(--MI_THEME-bg);
 	display: flex;
 	justify-content: center;
 	align-items: center;
diff --git a/packages/frontend/src/components/MkUserAnnouncementEditDialog.vue b/packages/frontend/src/components/MkUserAnnouncementEditDialog.vue
index 3c5f563aa0..26ba108244 100644
--- a/packages/frontend/src/components/MkUserAnnouncementEditDialog.vue
+++ b/packages/frontend/src/components/MkUserAnnouncementEditDialog.vue
@@ -25,9 +25,9 @@ SPDX-License-Identifier: AGPL-3.0-only
 				<MkRadios v-model="icon">
 					<template #label>{{ i18n.ts.icon }}</template>
 					<option value="info"><i class="ti ti-info-circle"></i></option>
-					<option value="warning"><i class="ti ti-alert-triangle" style="color: var(--warn);"></i></option>
-					<option value="error"><i class="ti ti-circle-x" style="color: var(--error);"></i></option>
-					<option value="success"><i class="ti ti-check" style="color: var(--success);"></i></option>
+					<option value="warning"><i class="ti ti-alert-triangle" style="color: var(--MI_THEME-warn);"></i></option>
+					<option value="error"><i class="ti ti-circle-x" style="color: var(--MI_THEME-error);"></i></option>
+					<option value="success"><i class="ti ti-check" style="color: var(--MI_THEME-success);"></i></option>
 				</MkRadios>
 				<MkRadios v-model="display">
 					<template #label>{{ i18n.ts.display }}</template>
@@ -141,7 +141,7 @@ async function del() {
 	bottom: 0;
 	left: 0;
 	padding: 12px;
-	border-top: solid 0.5px var(--divider);
+	border-top: solid 0.5px var(--MI_THEME-divider);
 	-webkit-backdrop-filter: var(--blur, blur(15px));
 	backdrop-filter: var(--blur, blur(15px));
 }
diff --git a/packages/frontend/src/components/MkUserCardMini.vue b/packages/frontend/src/components/MkUserCardMini.vue
index d3e77b2818..b333722dc2 100644
--- a/packages/frontend/src/components/MkUserCardMini.vue
+++ b/packages/frontend/src/components/MkUserCardMini.vue
@@ -49,7 +49,7 @@ $bodyInfoHieght: 16px;
 	display: flex;
 	align-items: center;
 	padding: 16px;
-	background: var(--panel);
+	background: var(--MI_THEME-panel);
 	border-radius: 8px;
 }
 
@@ -64,7 +64,7 @@ $bodyInfoHieght: 16px;
 	flex: 1;
 	overflow: hidden;
 	font-size: 0.9em;
-	color: var(--fg);
+	color: var(--MI_THEME-fg);
 	padding-right: 8px;
 }
 
diff --git a/packages/frontend/src/components/MkUserInfo.vue b/packages/frontend/src/components/MkUserInfo.vue
index f0b9606590..0164515a8a 100644
--- a/packages/frontend/src/components/MkUserInfo.vue
+++ b/packages/frontend/src/components/MkUserInfo.vue
@@ -69,7 +69,7 @@ defineProps<{
 	z-index: 2;
 	width: 58px;
 	height: 58px;
-	border: solid 4px var(--panel);
+	border: solid 4px var(--MI_THEME-panel);
 }
 
 .title {
@@ -90,7 +90,7 @@ defineProps<{
 	margin: 0;
 	line-height: 16px;
 	font-size: 0.8em;
-	color: var(--fg);
+	color: var(--MI_THEME-fg);
 	opacity: 0.7;
 }
 
@@ -108,7 +108,7 @@ defineProps<{
 .description {
 	padding: 16px;
 	font-size: 0.8em;
-	border-top: solid 0.5px var(--divider);
+	border-top: solid 0.5px var(--MI_THEME-divider);
 }
 
 .mfm {
@@ -120,7 +120,7 @@ defineProps<{
 
 .status {
 	padding: 10px 16px;
-	border-top: solid 0.5px var(--divider);
+	border-top: solid 0.5px var(--MI_THEME-divider);
 }
 
 .statusItem {
@@ -131,12 +131,12 @@ defineProps<{
 .statusItemLabel {
 	margin: 0;
 	font-size: 0.7em;
-	color: var(--fg);
+	color: var(--MI_THEME-fg);
 }
 
 .statusItemValue {
 	font-size: 1em;
-	color: var(--accent);
+	color: var(--MI_THEME-accent);
 }
 
 .follow {
diff --git a/packages/frontend/src/components/MkUserOnlineIndicator.vue b/packages/frontend/src/components/MkUserOnlineIndicator.vue
index c39a900bcf..5cebeea2f4 100644
--- a/packages/frontend/src/components/MkUserOnlineIndicator.vue
+++ b/packages/frontend/src/components/MkUserOnlineIndicator.vue
@@ -36,7 +36,7 @@ const text = computed(() => {
 
 <style lang="scss" module>
 .root {
-	box-shadow: 0 0 0 3px var(--panel);
+	box-shadow: 0 0 0 3px var(--MI_THEME-panel);
 	border-radius: 120%; // Blinkのバグか知らんけど、100%ぴったりにすると何故か若干楕円でレンダリングされる
 
 	&.status_online {
diff --git a/packages/frontend/src/components/MkUserPopup.vue b/packages/frontend/src/components/MkUserPopup.vue
index ea1241002e..740202f28b 100644
--- a/packages/frontend/src/components/MkUserPopup.vue
+++ b/packages/frontend/src/components/MkUserPopup.vue
@@ -18,7 +18,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 			</div>
 			<svg viewBox="0 0 128 128" :class="$style.avatarBack">
 				<g transform="matrix(1.6,0,0,1.6,-38.4,-51.2)">
-					<path d="M64,32C81.661,32 96,46.339 96,64C95.891,72.184 104,72 104,72C104,72 74.096,80 64,80C52.755,80 24,72 24,72C24,72 31.854,72.018 32,64C32,46.339 46.339,32 64,32Z" style="fill: var(--popup);"/>
+					<path d="M64,32C81.661,32 96,46.339 96,64C95.891,72.184 104,72 104,72C104,72 74.096,80 64,80C52.755,80 24,72 24,72C24,72 31.854,72.018 32,64C32,46.339 46.339,32 64,32Z" style="fill: var(--MI_THEME-popup);"/>
 				</g>
 			</svg>
 			<MkAvatar :class="$style.avatar" :user="user" indicator/>
@@ -197,8 +197,8 @@ onMounted(() => {
 	padding: 16px 26px;
 	font-size: 0.8em;
 	text-align: center;
-	border-top: solid 1px var(--divider);
-	border-bottom: solid 1px var(--divider);
+	border-top: solid 1px var(--MI_THEME-divider);
+	border-bottom: solid 1px var(--MI_THEME-divider);
 }
 
 .mfm {
@@ -220,7 +220,7 @@ onMounted(() => {
 
 .statusItemLabel {
 	font-size: 0.7em;
-	color: var(--fgTransparentWeak);
+	color: var(--MI_THEME-fgTransparentWeak);
 }
 
 .menu {
@@ -228,7 +228,7 @@ onMounted(() => {
 	top: 8px;
 	right: 44px;
 	padding: 6px;
-	background: var(--panel);
+	background: var(--MI_THEME-panel);
 	border-radius: 999px;
 }
 
diff --git a/packages/frontend/src/components/MkUserSelectDialog.vue b/packages/frontend/src/components/MkUserSelectDialog.vue
index 1374817c72..8e58a6c5a2 100644
--- a/packages/frontend/src/components/MkUserSelectDialog.vue
+++ b/packages/frontend/src/components/MkUserSelectDialog.vue
@@ -195,11 +195,11 @@ onMounted(() => {
 	font-size: 14px;
 
 	&:hover {
-		background: var(--X7);
+		background: var(--MI_THEME-X7);
 	}
 
 	&.selected {
-		background: var(--accent);
+		background: var(--MI_THEME-accent);
 		color: #fff;
 	}
 }
diff --git a/packages/frontend/src/components/MkUserSetupDialog.User.vue b/packages/frontend/src/components/MkUserSetupDialog.User.vue
index bb9af676e2..004edab630 100644
--- a/packages/frontend/src/components/MkUserSetupDialog.User.vue
+++ b/packages/frontend/src/components/MkUserSetupDialog.User.vue
@@ -61,7 +61,7 @@ async function follow() {
 	z-index: 2;
 	width: 58px;
 	height: 58px;
-	border: solid 4px var(--panel);
+	border: solid 4px var(--MI_THEME-panel);
 }
 
 .title {
@@ -82,7 +82,7 @@ async function follow() {
 	margin: 0;
 	line-height: 16px;
 	font-size: 0.8em;
-	color: var(--fg);
+	color: var(--MI_THEME-fg);
 	opacity: 0.7;
 }
 
@@ -99,7 +99,7 @@ async function follow() {
 }
 
 .footer {
-	border-top: solid 0.5px var(--divider);
+	border-top: solid 0.5px var(--MI_THEME-divider);
 	padding: 16px;
 }
 </style>
diff --git a/packages/frontend/src/components/MkUserSetupDialog.vue b/packages/frontend/src/components/MkUserSetupDialog.vue
index 1fb1eda039..b7261129ef 100644
--- a/packages/frontend/src/components/MkUserSetupDialog.vue
+++ b/packages/frontend/src/components/MkUserSetupDialog.vue
@@ -35,7 +35,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 					<MkAnimBg style="position: absolute; top: 0;" :scale="1.5"/>
 					<MkSpacer :marginMin="20" :marginMax="28">
 						<div class="_gaps" style="text-align: center;">
-							<i class="ti ti-confetti" style="display: block; margin: auto; font-size: 3em; color: var(--accent);"></i>
+							<i class="ti ti-confetti" style="display: block; margin: auto; font-size: 3em; color: var(--MI_THEME-accent);"></i>
 							<div style="font-size: 120%;">{{ i18n.ts._initialAccountSetting.accountCreated }}</div>
 							<div>{{ i18n.ts._initialAccountSetting.letsStartAccountSetup }}</div>
 							<MkButton primary rounded gradate style="margin: 16px auto 0 auto;" data-cy-user-setup-continue @click="page++">{{ i18n.ts._initialAccountSetting.profileSetting }} <i class="ti ti-arrow-right"></i></MkButton>
@@ -91,7 +91,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 				<div :class="$style.centerPage">
 					<MkSpacer :marginMin="20" :marginMax="28">
 						<div class="_gaps" style="text-align: center;">
-							<i class="ti ti-bell-ringing-2" style="display: block; margin: auto; font-size: 3em; color: var(--accent);"></i>
+							<i class="ti ti-bell-ringing-2" style="display: block; margin: auto; font-size: 3em; color: var(--MI_THEME-accent);"></i>
 							<div style="font-size: 120%;">{{ i18n.ts.pushNotification }}</div>
 							<div style="padding: 0 16px;">{{ i18n.tsx._initialAccountSetting.pushNotificationDescription({ name: instance.name ?? host }) }}</div>
 							<MkPushNotificationAllowButton primary showOnlyToRegister style="margin: 0 auto;"/>
@@ -108,7 +108,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 					<MkAnimBg style="position: absolute; top: 0;" :scale="1.5"/>
 					<MkSpacer :marginMin="20" :marginMax="28">
 						<div class="_gaps" style="text-align: center;">
-							<i class="ti ti-check" style="display: block; margin: auto; font-size: 3em; color: var(--accent);"></i>
+							<i class="ti ti-check" style="display: block; margin: auto; font-size: 3em; color: var(--MI_THEME-accent);"></i>
 							<div style="font-size: 120%;">{{ i18n.ts._initialAccountSetting.initialAccountSettingCompleted }}</div>
 							<div>{{ i18n.tsx._initialAccountSetting.youCanContinueTutorial({ name: instance.name ?? host }) }}</div>
 							<div class="_buttonsCenter" style="margin-top: 16px;">
@@ -223,7 +223,7 @@ async function later(later: boolean) {
 
 .progressBarValue {
 	height: 100%;
-	background: linear-gradient(90deg, var(--buttonGradateA), var(--buttonGradateB));
+	background: linear-gradient(90deg, var(--MI_THEME-buttonGradateA), var(--MI_THEME-buttonGradateB));
 	transition: all 0.5s cubic-bezier(0,.5,.5,1);
 }
 
@@ -252,7 +252,7 @@ async function later(later: boolean) {
 	left: 0;
 	flex-shrink: 0;
 	padding: 12px;
-	border-top: solid 0.5px var(--divider);
+	border-top: solid 0.5px var(--MI_THEME-divider);
 	-webkit-backdrop-filter: blur(15px);
 	backdrop-filter: blur(15px);
 }
diff --git a/packages/frontend/src/components/MkVisibilityPicker.vue b/packages/frontend/src/components/MkVisibilityPicker.vue
index 75066bbc32..650e639c4f 100644
--- a/packages/frontend/src/components/MkVisibilityPicker.vue
+++ b/packages/frontend/src/components/MkVisibilityPicker.vue
@@ -124,7 +124,7 @@ function choose(visibility: typeof Misskey.noteVisibilities[number]): void {
 	}
 
 	&.active {
-		color: var(--accent);
+		color: var(--MI_THEME-accent);
 	}
 }
 
diff --git a/packages/frontend/src/components/MkVisitorDashboard.ActiveUsersChart.vue b/packages/frontend/src/components/MkVisitorDashboard.ActiveUsersChart.vue
index cab42cd59d..d098dad9a1 100644
--- a/packages/frontend/src/components/MkVisitorDashboard.ActiveUsersChart.vue
+++ b/packages/frontend/src/components/MkVisitorDashboard.ActiveUsersChart.vue
@@ -62,7 +62,7 @@ async function renderChart() {
 	const vLineColor = defaultStore.state.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
 
 	const computedStyle = getComputedStyle(document.documentElement);
-	const accent = tinycolor(computedStyle.getPropertyValue('--accent')).toHexString();
+	const accent = tinycolor(computedStyle.getPropertyValue('--MI_THEME-accent')).toHexString();
 
 	const colorRead = accent;
 	const colorWrite = '#2ecc71';
diff --git a/packages/frontend/src/components/MkVisitorDashboard.vue b/packages/frontend/src/components/MkVisitorDashboard.vue
index a6c8baeaaa..91e2898798 100644
--- a/packages/frontend/src/components/MkVisitorDashboard.vue
+++ b/packages/frontend/src/components/MkVisitorDashboard.vue
@@ -106,7 +106,7 @@ function showMenu(ev: MouseEvent) {
 
 .panel {
 	position: relative;
-	background: var(--panel);
+	background: var(--MI_THEME-panel);
 	border-radius: var(--radius);
 	box-shadow: 0 12px 32px rgb(0 0 0 / 25%);
 }
@@ -178,14 +178,14 @@ function showMenu(ev: MouseEvent) {
 }
 
 .statsItemLabel {
-	color: var(--fgTransparentWeak);
+	color: var(--MI_THEME-fgTransparentWeak);
 	font-size: 0.9em;
 }
 
 .statsItemCount {
 	font-weight: bold;
 	font-size: 1.2em;
-	color: var(--accent);
+	color: var(--MI_THEME-accent);
 }
 
 .tl {
@@ -194,7 +194,7 @@ function showMenu(ev: MouseEvent) {
 
 .tlHeader {
 	padding: 12px 16px;
-	border-bottom: solid 1px var(--divider);
+	border-bottom: solid 1px var(--MI_THEME-divider);
 }
 
 .tlBody {
diff --git a/packages/frontend/src/components/MkWaitingDialog.vue b/packages/frontend/src/components/MkWaitingDialog.vue
index 60b75b6d30..62e187f172 100644
--- a/packages/frontend/src/components/MkWaitingDialog.vue
+++ b/packages/frontend/src/components/MkWaitingDialog.vue
@@ -47,7 +47,7 @@ watch(() => props.showing, () => {
 	padding: 32px;
 	box-sizing: border-box;
 	text-align: center;
-	background: var(--panel);
+	background: var(--MI_THEME-panel);
 	border-radius: var(--radius);
 	width: 250px;
 
@@ -65,7 +65,7 @@ watch(() => props.showing, () => {
 	font-size: 32px;
 
 	&.success {
-		color: var(--accent);
+		color: var(--MI_THEME-accent);
 	}
 
 	&.waiting {
diff --git a/packages/frontend/src/components/MkWindow.vue b/packages/frontend/src/components/MkWindow.vue
index 08906a1205..dd92952a35 100644
--- a/packages/frontend/src/components/MkWindow.vue
+++ b/packages/frontend/src/components/MkWindow.vue
@@ -514,10 +514,10 @@ defineExpose({
 	flex-shrink: 0;
 	user-select: none;
 	height: var(--height);
-	background: var(--windowHeader);
+	background: var(--MI_THEME-windowHeader);
 	-webkit-backdrop-filter: var(--blur, blur(15px));
 	backdrop-filter: var(--blur, blur(15px));
-	//border-bottom: solid 1px var(--divider);
+	//border-bottom: solid 1px var(--MI_THEME-divider);
 	font-size: 90%;
 	font-weight: bold;
 
@@ -531,11 +531,11 @@ defineExpose({
 	width: var(--height);
 
 	&:hover {
-		color: var(--fgHighlighted);
+		color: var(--MI_THEME-fgHighlighted);
 	}
 
 	&.highlighted {
-		color: var(--accent);
+		color: var(--MI_THEME-accent);
 	}
 }
 
@@ -560,7 +560,7 @@ defineExpose({
 .content {
 	flex: 1;
 	overflow: auto;
-	background: var(--panel);
+	background: var(--MI_THEME-panel);
 	container-type: size;
 }
 
diff --git a/packages/frontend/src/components/form/link.vue b/packages/frontend/src/components/form/link.vue
index d6585bf4a5..8fa9e4affb 100644
--- a/packages/frontend/src/components/form/link.vue
+++ b/packages/frontend/src/components/form/link.vue
@@ -51,18 +51,18 @@ const props = defineProps<{
 	width: 100%;
 	box-sizing: border-box;
 	padding: 10px 14px;
-	background: var(--folderHeaderBg);
+	background: var(--MI_THEME-folderHeaderBg);
 	border-radius: 6px;
 	font-size: 0.9em;
 
 	&:hover {
 		text-decoration: none;
-		background: var(--folderHeaderHoverBg);
+		background: var(--MI_THEME-folderHeaderHoverBg);
 	}
 
 	&.active {
-		color: var(--accent);
-		background: var(--folderHeaderHoverBg);
+		color: var(--MI_THEME-accent);
+		background: var(--MI_THEME-folderHeaderHoverBg);
 	}
 }
 
@@ -70,7 +70,7 @@ const props = defineProps<{
 	margin-right: 0.75em;
 	flex-shrink: 0;
 	text-align: center;
-	color: var(--fgTransparentWeak);
+	color: var(--MI_THEME-fgTransparentWeak);
 
 	&:empty {
 		display: none;
diff --git a/packages/frontend/src/components/form/section.vue b/packages/frontend/src/components/form/section.vue
index ad37daa265..5fca3acc31 100644
--- a/packages/frontend/src/components/form/section.vue
+++ b/packages/frontend/src/components/form/section.vue
@@ -21,8 +21,8 @@ defineProps<{
 
 <style lang="scss" module>
 .root {
-	border-top: solid 0.5px var(--divider);
-	//border-bottom: solid 0.5px var(--divider);
+	border-top: solid 0.5px var(--MI_THEME-divider);
+	//border-bottom: solid 0.5px var(--MI_THEME-divider);
 }
 
 .rootFirst {
@@ -49,7 +49,7 @@ defineProps<{
 
 .description {
 	font-size: 0.85em;
-	color: var(--fgTransparentWeak);
+	color: var(--MI_THEME-fgTransparentWeak);
 	margin: 0 0 8px 0;
 }
 </style>
diff --git a/packages/frontend/src/components/form/slot.vue b/packages/frontend/src/components/form/slot.vue
index f54db0ca82..da94b7abbb 100644
--- a/packages/frontend/src/components/form/slot.vue
+++ b/packages/frontend/src/components/form/slot.vue
@@ -35,7 +35,7 @@ function focus() {
 .caption {
 	font-size: 0.85em;
 	padding: 8px 0 0 0;
-	color: var(--fgTransparentWeak);
+	color: var(--MI_THEME-fgTransparentWeak);
 
 	&:empty {
 		display: none;
diff --git a/packages/frontend/src/components/global/MkAd.vue b/packages/frontend/src/components/global/MkAd.vue
index f0e943960d..b525a81fbe 100644
--- a/packages/frontend/src/components/global/MkAd.vue
+++ b/packages/frontend/src/components/global/MkAd.vue
@@ -191,7 +191,7 @@ function reduceFrequency(): void {
 	right: 1px;
 	display: grid;
 	place-content: center;
-	background: var(--panel);
+	background: var(--MI_THEME-panel);
 	border-radius: 100%;
 	padding: 2px;
 }
@@ -210,7 +210,7 @@ function reduceFrequency(): void {
 	padding: 8px;
 	margin: 0 auto;
 	max-width: 400px;
-	border: solid 1px var(--divider);
+	border: solid 1px var(--MI_THEME-divider);
 }
 
 .menuButton {
diff --git a/packages/frontend/src/components/global/MkLoading.vue b/packages/frontend/src/components/global/MkLoading.vue
index 49d8ace37b..47d797606b 100644
--- a/packages/frontend/src/components/global/MkLoading.vue
+++ b/packages/frontend/src/components/global/MkLoading.vue
@@ -56,7 +56,7 @@ const props = withDefaults(defineProps<{
 	--size: 38px;
 
 	&.colored {
-		color: var(--accent);
+		color: var(--MI_THEME-accent);
 	}
 
 	&.inline {
diff --git a/packages/frontend/src/components/global/MkMfm.ts b/packages/frontend/src/components/global/MkMfm.ts
index 1beb8874e0..0d4ae8cacb 100644
--- a/packages/frontend/src/components/global/MkMfm.ts
+++ b/packages/frontend/src/components/global/MkMfm.ts
@@ -31,8 +31,8 @@ const QUOTE_STYLE = `
 display: block;
 margin: 8px;
 padding: 6px 0 6px 12px;
-color: var(--fg);
-border-left: solid 3px var(--fg);
+color: var(--MI_THEME-fg);
+border-left: solid 3px var(--MI_THEME-fg);
 opacity: 0.7;
 `.split('\n').join(' ');
 
@@ -270,7 +270,7 @@ export default function (props: MfmProps, { emit }: { emit: SetupContext<MfmEven
 					}
 					case 'border': {
 						let color = validColor(token.props.args.color);
-						color = color ? `#${color}` : 'var(--accent)';
+						color = color ? `#${color}` : 'var(--MI_THEME-accent)';
 						let b_style = token.props.args.style;
 						if (
 							typeof b_style !== 'string' ||
@@ -303,7 +303,7 @@ export default function (props: MfmProps, { emit }: { emit: SetupContext<MfmEven
 						const child = token.children[0];
 						const unixtime = parseInt(child.type === 'text' ? child.props.text : '');
 						return h('span', {
-							style: 'display: inline-block; font-size: 90%; border: solid 1px var(--divider); border-radius: 999px; padding: 4px 10px 4px 6px;',
+							style: 'display: inline-block; font-size: 90%; border: solid 1px var(--MI_THEME-divider); border-radius: 999px; padding: 4px 10px 4px 6px;',
 						}, [
 							h('i', {
 								class: 'ti ti-clock',
@@ -377,7 +377,7 @@ export default function (props: MfmProps, { emit }: { emit: SetupContext<MfmEven
 				return [h(MkA, {
 					key: Math.random(),
 					to: isNote ? `/tags/${encodeURIComponent(token.props.hashtag)}` : `/user-tags/${encodeURIComponent(token.props.hashtag)}`,
-					style: 'color:var(--hashtag);',
+					style: 'color:var(--MI_THEME-hashtag);',
 					behavior: props.linkNavigationBehavior,
 				}, `#${token.props.hashtag}`)];
 			}
diff --git a/packages/frontend/src/components/global/MkPageHeader.tabs.vue b/packages/frontend/src/components/global/MkPageHeader.tabs.vue
index fcc46cc345..adf8638dae 100644
--- a/packages/frontend/src/components/global/MkPageHeader.tabs.vue
+++ b/packages/frontend/src/components/global/MkPageHeader.tabs.vue
@@ -247,7 +247,7 @@ onUnmounted(() => {
 	position: absolute;
 	bottom: 0;
 	height: 3px;
-	background: var(--accent);
+	background: var(--MI_THEME-accent);
 	border-radius: 999px;
 	transition: none;
 	pointer-events: none;
diff --git a/packages/frontend/src/components/global/MkPageHeader.vue b/packages/frontend/src/components/global/MkPageHeader.vue
index f1a451808f..e032313b02 100644
--- a/packages/frontend/src/components/global/MkPageHeader.vue
+++ b/packages/frontend/src/components/global/MkPageHeader.vue
@@ -99,7 +99,7 @@ function onTabClick(): void {
 }
 
 const calcBg = () => {
-	const rawBg = 'var(--bg)';
+	const rawBg = 'var(--MI_THEME-bg)';
 	const tinyBg = tinycolor(rawBg.startsWith('var(') ? getComputedStyle(document.documentElement).getPropertyValue(rawBg.slice(4, -1)) : rawBg);
 	tinyBg.setAlpha(0.85);
 	bg.value = tinyBg.toRgbString();
@@ -132,7 +132,7 @@ onUnmounted(() => {
 .root {
 	-webkit-backdrop-filter: var(--blur, blur(15px));
 	backdrop-filter: var(--blur, blur(15px));
-	border-bottom: solid 0.5px var(--divider);
+	border-bottom: solid 0.5px var(--MI_THEME-divider);
 	width: 100%;
 }
 
@@ -230,7 +230,7 @@ onUnmounted(() => {
 	}
 
 	&.highlighted {
-		color: var(--accent);
+		color: var(--MI_THEME-accent);
 	}
 }
 
diff --git a/packages/frontend/src/components/global/MkTime.vue b/packages/frontend/src/components/global/MkTime.vue
index 50bec990a1..f600f7eed2 100644
--- a/packages/frontend/src/components/global/MkTime.vue
+++ b/packages/frontend/src/components/global/MkTime.vue
@@ -99,10 +99,10 @@ if (!invalid && props.origin === null && (props.mode === 'relative' || props.mod
 
 <style lang="scss" module>
 .old1 {
-	color: var(--warn);
+	color: var(--MI_THEME-warn);
 }
 
 .old1.old2 {
-	color: var(--error);
+	color: var(--MI_THEME-error);
 }
 </style>
diff --git a/packages/frontend/src/components/page/page.dynamic.vue b/packages/frontend/src/components/page/page.dynamic.vue
index 8c511a690d..c355cd07d7 100644
--- a/packages/frontend/src/components/page/page.dynamic.vue
+++ b/packages/frontend/src/components/page/page.dynamic.vue
@@ -27,7 +27,7 @@ const props = defineProps<{
 
 <style lang="scss" module>
 .root {
-	border: 1px solid var(--divider);
+	border: 1px solid var(--MI_THEME-divider);
 	border-radius: var(--radius);
 	padding: var(--margin);
 	text-align: center;
diff --git a/packages/frontend/src/components/page/page.image.vue b/packages/frontend/src/components/page/page.image.vue
index fc1ce9fc7b..c4bedcdb54 100644
--- a/packages/frontend/src/components/page/page.image.vue
+++ b/packages/frontend/src/components/page/page.image.vue
@@ -28,7 +28,7 @@ onMounted(() => {
 
 <style lang="scss" module>
 .root {
-	border: 1px solid var(--divider);
+	border: 1px solid var(--MI_THEME-divider);
 	border-radius: var(--radius);
 	overflow: hidden;
 }
diff --git a/packages/frontend/src/components/page/page.note.vue b/packages/frontend/src/components/page/page.note.vue
index b5ba407806..4a1be9b772 100644
--- a/packages/frontend/src/components/page/page.note.vue
+++ b/packages/frontend/src/components/page/page.note.vue
@@ -35,7 +35,7 @@ onMounted(() => {
 
 <style lang="scss" module>
 .root {
-	border: 1px solid var(--divider);
+	border: 1px solid var(--MI_THEME-divider);
 	border-radius: var(--radius);
 }
 </style>
diff --git a/packages/frontend/src/directives/adaptive-bg.ts b/packages/frontend/src/directives/adaptive-bg.ts
index 23fd1bddf4..45891de889 100644
--- a/packages/frontend/src/directives/adaptive-bg.ts
+++ b/packages/frontend/src/directives/adaptive-bg.ts
@@ -21,7 +21,7 @@ export default {
 		const myBg = window.getComputedStyle(src).backgroundColor;
 
 		if (parentBg === myBg) {
-			src.style.backgroundColor = 'var(--bg)';
+			src.style.backgroundColor = 'var(--MI_THEME-bg)';
 		} else {
 			src.style.backgroundColor = myBg;
 		}
diff --git a/packages/frontend/src/directives/adaptive-border.ts b/packages/frontend/src/directives/adaptive-border.ts
index b436075fcd..685ca38e96 100644
--- a/packages/frontend/src/directives/adaptive-border.ts
+++ b/packages/frontend/src/directives/adaptive-border.ts
@@ -21,7 +21,7 @@ export default {
 		const myBg = window.getComputedStyle(src).backgroundColor;
 
 		if (parentBg === myBg) {
-			src.style.borderColor = 'var(--divider)';
+			src.style.borderColor = 'var(--MI_THEME-divider)';
 		} else {
 			src.style.borderColor = myBg;
 		}
diff --git a/packages/frontend/src/directives/panel.ts b/packages/frontend/src/directives/panel.ts
index bbcc220e09..7b5969c679 100644
--- a/packages/frontend/src/directives/panel.ts
+++ b/packages/frontend/src/directives/panel.ts
@@ -18,12 +18,12 @@ export default {
 
 		const parentBg = getBgColor(src.parentElement);
 
-		const myBg = getComputedStyle(document.documentElement).getPropertyValue('--panel');
+		const myBg = getComputedStyle(document.documentElement).getPropertyValue('--MI_THEME-panel');
 
 		if (parentBg === myBg) {
-			src.style.backgroundColor = 'var(--bg)';
+			src.style.backgroundColor = 'var(--MI_THEME-bg)';
 		} else {
-			src.style.backgroundColor = 'var(--panel)';
+			src.style.backgroundColor = 'var(--MI_THEME-panel)';
 		}
 	},
 } as Directive;
diff --git a/packages/frontend/src/pages/about-misskey.vue b/packages/frontend/src/pages/about-misskey.vue
index b481fd590c..a66d580db9 100644
--- a/packages/frontend/src/pages/about-misskey.vue
+++ b/packages/frontend/src/pages/about-misskey.vue
@@ -529,17 +529,17 @@ definePageMetadata(() => ({
 	display: flex;
 	align-items: center;
 	padding: 12px;
-	background: var(--buttonBg);
+	background: var(--MI_THEME-buttonBg);
 	border-radius: 6px;
 
 	&:hover {
 		text-decoration: none;
-		background: var(--buttonHoverBg);
+		background: var(--MI_THEME-buttonHoverBg);
 	}
 
 	&.active {
-		color: var(--accent);
-		background: var(--buttonHoverBg);
+		color: var(--MI_THEME-accent);
+		background: var(--MI_THEME-buttonHoverBg);
 	}
 }
 
@@ -562,7 +562,7 @@ definePageMetadata(() => ({
 	display: flex;
 	align-items: center;
 	padding: 12px;
-	background: var(--buttonBg);
+	background: var(--MI_THEME-buttonBg);
 	border-radius: 6px;
 }
 
diff --git a/packages/frontend/src/pages/about.overview.vue b/packages/frontend/src/pages/about.overview.vue
index b645506eff..c19757f88f 100644
--- a/packages/frontend/src/pages/about.overview.vue
+++ b/packages/frontend/src/pages/about.overview.vue
@@ -147,7 +147,7 @@ const initStats = () => misskeyApi('stats', {});
 	text-align: center;
 	border-radius: 10px;
 	overflow: clip;
-	background-color: var(--panel);
+	background-color: var(--MI_THEME-panel);
 	background-size: cover;
 	background-position: center center;
 }
@@ -189,8 +189,8 @@ const initStats = () => misskeyApi('stats', {});
 		width: 32px;
 		height: 32px;
 		line-height: 32px;
-		background-color: var(--accentedBg);
-		color: var(--accent);
+		background-color: var(--MI_THEME-accentedBg);
+		color: var(--MI_THEME-accent);
 		font-size: 13px;
 		font-weight: bold;
 		align-items: center;
diff --git a/packages/frontend/src/pages/admin-user.vue b/packages/frontend/src/pages/admin-user.vue
index 033634396e..d33b116059 100644
--- a/packages/frontend/src/pages/admin-user.vue
+++ b/packages/frontend/src/pages/admin-user.vue
@@ -159,9 +159,9 @@ SPDX-License-Identifier: AGPL-3.0-only
 							<div v-for="announcement in items" :key="announcement.id" v-panel :class="$style.announcementItem" @click="editAnnouncement(announcement)">
 								<span style="margin-right: 0.5em;">
 									<i v-if="announcement.icon === 'info'" class="ti ti-info-circle"></i>
-									<i v-else-if="announcement.icon === 'warning'" class="ti ti-alert-triangle" style="color: var(--warn);"></i>
-									<i v-else-if="announcement.icon === 'error'" class="ti ti-circle-x" style="color: var(--error);"></i>
-									<i v-else-if="announcement.icon === 'success'" class="ti ti-check" style="color: var(--success);"></i>
+									<i v-else-if="announcement.icon === 'warning'" class="ti ti-alert-triangle" style="color: var(--MI_THEME-warn);"></i>
+									<i v-else-if="announcement.icon === 'error'" class="ti ti-circle-x" style="color: var(--MI_THEME-error);"></i>
+									<i v-else-if="announcement.icon === 'success'" class="ti ti-check" style="color: var(--MI_THEME-success);"></i>
 								</span>
 								<span>{{ announcement.title }}</span>
 								<span v-if="announcement.reads > 0" style="margin-left: auto; opacity: 0.7;">{{ i18n.ts.messageRead }}</span>
@@ -582,18 +582,18 @@ definePageMetadata(() => ({
 			}
 
 			> .suspended {
-				color: var(--error);
-				border-color: var(--error);
+				color: var(--MI_THEME-error);
+				border-color: var(--MI_THEME-error);
 			}
 
 			> .silenced {
-				color: var(--warn);
-				border-color: var(--warn);
+				color: var(--MI_THEME-warn);
+				border-color: var(--MI_THEME-warn);
 			}
 
 			> .moderator {
-				color: var(--success);
-				border-color: var(--success);
+				color: var(--MI_THEME-success);
+				border-color: var(--MI_THEME-success);
 			}
 		}
 	}
@@ -640,7 +640,7 @@ definePageMetadata(() => ({
 .roleItemSub {
 	padding: 6px 12px;
 	font-size: 85%;
-	color: var(--fgTransparentWeak);
+	color: var(--MI_THEME-fgTransparentWeak);
 }
 
 .roleUnassign {
diff --git a/packages/frontend/src/pages/admin/RolesEditorFormula.vue b/packages/frontend/src/pages/admin/RolesEditorFormula.vue
index f001a4ac20..dc2862d225 100644
--- a/packages/frontend/src/pages/admin/RolesEditorFormula.vue
+++ b/packages/frontend/src/pages/admin/RolesEditorFormula.vue
@@ -155,12 +155,12 @@ function removeSelf() {
 }
 
 .item {
-	border: solid 2px var(--divider);
+	border: solid 2px var(--MI_THEME-divider);
 	border-radius: var(--radius);
 	padding: 12px;
 
 	&:hover {
-		border-color: var(--accent);
+		border-color: var(--MI_THEME-accent);
 	}
 }
 </style>
diff --git a/packages/frontend/src/pages/admin/_header_.vue b/packages/frontend/src/pages/admin/_header_.vue
index d22e078c2a..36fe483771 100644
--- a/packages/frontend/src/pages/admin/_header_.vue
+++ b/packages/frontend/src/pages/admin/_header_.vue
@@ -119,7 +119,7 @@ function onTabClick(tab: Tab, ev: MouseEvent): void {
 }
 
 const calcBg = () => {
-	const rawBg = pageMetadata.value?.bg ?? 'var(--bg)';
+	const rawBg = pageMetadata.value?.bg ?? 'var(--MI_THEME-bg)';
 	const tinyBg = tinycolor(rawBg.startsWith('var(') ? getComputedStyle(document.documentElement).getPropertyValue(rawBg.slice(4, -1)) : rawBg);
 	tinyBg.setAlpha(0.85);
 	bg.value = tinyBg.toRgbString();
@@ -189,7 +189,7 @@ onUnmounted(() => {
 			}
 
 			&.highlighted {
-				color: var(--accent);
+				color: var(--MI_THEME-accent);
 			}
 		}
 
@@ -286,7 +286,7 @@ onUnmounted(() => {
 			position: absolute;
 			bottom: 0;
 			height: 3px;
-			background: var(--accent);
+			background: var(--MI_THEME-accent);
 			border-radius: 999px;
 			transition: all 0.2s ease;
 			pointer-events: none;
diff --git a/packages/frontend/src/pages/admin/abuse-report/notification-recipient.editor.vue b/packages/frontend/src/pages/admin/abuse-report/notification-recipient.editor.vue
index 827e22e8ae..f70b46b84a 100644
--- a/packages/frontend/src/pages/admin/abuse-report/notification-recipient.editor.vue
+++ b/packages/frontend/src/pages/admin/abuse-report/notification-recipient.editor.vue
@@ -294,8 +294,8 @@ onMounted(async () => {
 	bottom: 0;
 	left: 0;
 	padding: 12px;
-	border-top: solid 0.5px var(--divider);
-	background: var(--acrylicBg);
+	border-top: solid 0.5px var(--MI_THEME-divider);
+	background: var(--MI_THEME-acrylicBg);
 	-webkit-backdrop-filter: var(--blur, blur(15px));
 	backdrop-filter: var(--blur, blur(15px));
 }
diff --git a/packages/frontend/src/pages/admin/abuse-report/notification-recipient.item.vue b/packages/frontend/src/pages/admin/abuse-report/notification-recipient.item.vue
index 0b86808faf..36d586bd23 100644
--- a/packages/frontend/src/pages/admin/abuse-report/notification-recipient.item.vue
+++ b/packages/frontend/src/pages/admin/abuse-report/notification-recipient.item.vue
@@ -87,7 +87,7 @@ function onDeleteButtonClicked() {
 }
 
 .rightDivider {
-	border-right: 0.5px solid var(--divider);
+	border-right: 0.5px solid var(--MI_THEME-divider);
 }
 
 .recipientButtons {
@@ -108,7 +108,7 @@ function onDeleteButtonClicked() {
 	padding: 8px;
 
 	&:hover {
-		background-color: var(--buttonBg);
+		background-color: var(--MI_THEME-buttonBg);
 	}
 }
 </style>
diff --git a/packages/frontend/src/pages/admin/announcements.vue b/packages/frontend/src/pages/admin/announcements.vue
index fd37311b21..e420586017 100644
--- a/packages/frontend/src/pages/admin/announcements.vue
+++ b/packages/frontend/src/pages/admin/announcements.vue
@@ -24,9 +24,9 @@ SPDX-License-Identifier: AGPL-3.0-only
 					<template #label>{{ announcement.title }}</template>
 					<template #icon>
 						<i v-if="announcement.icon === 'info'" class="ti ti-info-circle"></i>
-						<i v-else-if="announcement.icon === 'warning'" class="ti ti-alert-triangle" style="color: var(--warn);"></i>
-						<i v-else-if="announcement.icon === 'error'" class="ti ti-circle-x" style="color: var(--error);"></i>
-						<i v-else-if="announcement.icon === 'success'" class="ti ti-check" style="color: var(--success);"></i>
+						<i v-else-if="announcement.icon === 'warning'" class="ti ti-alert-triangle" style="color: var(--MI_THEME-warn);"></i>
+						<i v-else-if="announcement.icon === 'error'" class="ti ti-circle-x" style="color: var(--MI_THEME-error);"></i>
+						<i v-else-if="announcement.icon === 'success'" class="ti ti-check" style="color: var(--MI_THEME-success);"></i>
 					</template>
 					<template #caption>{{ announcement.text }}</template>
 					<template #footer>
@@ -51,9 +51,9 @@ SPDX-License-Identifier: AGPL-3.0-only
 						<MkRadios v-model="announcement.icon">
 							<template #label>{{ i18n.ts.icon }}</template>
 							<option value="info"><i class="ti ti-info-circle"></i></option>
-							<option value="warning"><i class="ti ti-alert-triangle" style="color: var(--warn);"></i></option>
-							<option value="error"><i class="ti ti-circle-x" style="color: var(--error);"></i></option>
-							<option value="success"><i class="ti ti-check" style="color: var(--success);"></i></option>
+							<option value="warning"><i class="ti ti-alert-triangle" style="color: var(--MI_THEME-warn);"></i></option>
+							<option value="error"><i class="ti ti-circle-x" style="color: var(--MI_THEME-error);"></i></option>
+							<option value="success"><i class="ti ti-check" style="color: var(--MI_THEME-success);"></i></option>
 						</MkRadios>
 						<MkRadios v-model="announcement.display">
 							<template #label>{{ i18n.ts.display }}</template>
diff --git a/packages/frontend/src/pages/admin/index.vue b/packages/frontend/src/pages/admin/index.vue
index 61745e0ff3..8a206a2f79 100644
--- a/packages/frontend/src/pages/admin/index.vue
+++ b/packages/frontend/src/pages/admin/index.vue
@@ -331,7 +331,7 @@ defineExpose({
 			width: 32%;
 			max-width: 280px;
 			box-sizing: border-box;
-			border-right: solid 0.5px var(--divider);
+			border-right: solid 0.5px var(--MI_THEME-divider);
 			overflow: auto;
 			height: 100%;
 		}
diff --git a/packages/frontend/src/pages/admin/modlog.ModLog.vue b/packages/frontend/src/pages/admin/modlog.ModLog.vue
index 6cf95e936e..ddbd293c3a 100644
--- a/packages/frontend/src/pages/admin/modlog.ModLog.vue
+++ b/packages/frontend/src/pages/admin/modlog.ModLog.vue
@@ -205,14 +205,14 @@ const props = defineProps<{
 }
 
 .logYellow {
-	color: var(--warn);
+	color: var(--MI_THEME-warn);
 }
 
 .logRed {
-	color: var(--error);
+	color: var(--MI_THEME-error);
 }
 
 .logGreen {
-	color: var(--success);
+	color: var(--MI_THEME-success);
 }
 </style>
diff --git a/packages/frontend/src/pages/admin/overview.ap-requests.vue b/packages/frontend/src/pages/admin/overview.ap-requests.vue
index 4bbb9210af..570fcddc07 100644
--- a/packages/frontend/src/pages/admin/overview.ap-requests.vue
+++ b/packages/frontend/src/pages/admin/overview.ap-requests.vue
@@ -278,7 +278,7 @@ onMounted(async () => {
 				padding: 16px;
 
 				&:first-child {
-					border-bottom: solid 0.5px var(--divider);
+					border-bottom: solid 0.5px var(--MI_THEME-divider);
 				}
 			}
 		}
diff --git a/packages/frontend/src/pages/admin/overview.federation.vue b/packages/frontend/src/pages/admin/overview.federation.vue
index 022b392d2d..0896859f3c 100644
--- a/packages/frontend/src/pages/admin/overview.federation.vue
+++ b/packages/frontend/src/pages/admin/overview.federation.vue
@@ -151,8 +151,8 @@ onMounted(async () => {
 					height: 100%;
 					aspect-ratio: 1;
 					margin-right: 12px;
-					background: var(--accentedBg);
-					color: var(--accent);
+					background: var(--MI_THEME-accentedBg);
+					color: var(--MI_THEME-accent);
 					border-radius: 10px;
 				}
 
diff --git a/packages/frontend/src/pages/admin/overview.pie.vue b/packages/frontend/src/pages/admin/overview.pie.vue
index c7a9f2a702..a21ec6c464 100644
--- a/packages/frontend/src/pages/admin/overview.pie.vue
+++ b/packages/frontend/src/pages/admin/overview.pie.vue
@@ -41,7 +41,7 @@ onMounted(() => {
 			labels: props.data.map(x => x.name),
 			datasets: [{
 				backgroundColor: props.data.map(x => x.color),
-				borderColor: getComputedStyle(document.documentElement).getPropertyValue('--panel'),
+				borderColor: getComputedStyle(document.documentElement).getPropertyValue('--MI_THEME-panel'),
 				borderWidth: 2,
 				hoverOffset: 0,
 				data: props.data.map(x => x.value),
diff --git a/packages/frontend/src/pages/admin/overview.queue.vue b/packages/frontend/src/pages/admin/overview.queue.vue
index fb190f5325..98d1b8d7f6 100644
--- a/packages/frontend/src/pages/admin/overview.queue.vue
+++ b/packages/frontend/src/pages/admin/overview.queue.vue
@@ -119,7 +119,7 @@ onUnmounted(() => {
 			> .chart {
 				min-width: 0;
 				padding: 16px;
-				background: var(--panel);
+				background: var(--MI_THEME-panel);
 				border-radius: var(--radius);
 
 				> .title {
diff --git a/packages/frontend/src/pages/admin/overview.stats.vue b/packages/frontend/src/pages/admin/overview.stats.vue
index 0f4707f08d..222e9f4673 100644
--- a/packages/frontend/src/pages/admin/overview.stats.vue
+++ b/packages/frontend/src/pages/admin/overview.stats.vue
@@ -114,8 +114,8 @@ onMounted(async () => {
 				height: 100%;
 				aspect-ratio: 1;
 				margin-right: 12px;
-				background: var(--accentedBg);
-				color: var(--accent);
+				background: var(--MI_THEME-accentedBg);
+				color: var(--MI_THEME-accent);
 				border-radius: 10px;
 			}
 
diff --git a/packages/frontend/src/pages/admin/queue.chart.vue b/packages/frontend/src/pages/admin/queue.chart.vue
index 960a263a86..700865c91c 100644
--- a/packages/frontend/src/pages/admin/queue.chart.vue
+++ b/packages/frontend/src/pages/admin/queue.chart.vue
@@ -135,7 +135,7 @@ onUnmounted(() => {
 .chart {
 	min-width: 0;
 	padding: 16px;
-	background: var(--panel);
+	background: var(--MI_THEME-panel);
 	border-radius: var(--radius);
 }
 
diff --git a/packages/frontend/src/pages/admin/relays.vue b/packages/frontend/src/pages/admin/relays.vue
index 04982eea1f..17e99e6593 100644
--- a/packages/frontend/src/pages/admin/relays.vue
+++ b/packages/frontend/src/pages/admin/relays.vue
@@ -11,8 +11,8 @@ SPDX-License-Identifier: AGPL-3.0-only
 			<div v-for="relay in relays" :key="relay.inbox" class="relaycxt _panel" style="padding: 16px;">
 				<div>{{ relay.inbox }}</div>
 				<div style="margin: 8px 0;">
-					<i v-if="relay.status === 'accepted'" class="ti ti-check" :class="$style.icon" style="color: var(--success);"></i>
-					<i v-else-if="relay.status === 'rejected'" class="ti ti-ban" :class="$style.icon" style="color: var(--error);"></i>
+					<i v-if="relay.status === 'accepted'" class="ti ti-check" :class="$style.icon" style="color: var(--MI_THEME-success);"></i>
+					<i v-else-if="relay.status === 'rejected'" class="ti ti-ban" :class="$style.icon" style="color: var(--MI_THEME-error);"></i>
 					<i v-else class="ti ti-clock" :class="$style.icon"></i>
 					<span>{{ i18n.ts._relayStatus[relay.status] }}</span>
 				</div>
diff --git a/packages/frontend/src/pages/admin/roles.role.vue b/packages/frontend/src/pages/admin/roles.role.vue
index 8b3c906d8a..1c237a69b4 100644
--- a/packages/frontend/src/pages/admin/roles.role.vue
+++ b/packages/frontend/src/pages/admin/roles.role.vue
@@ -184,7 +184,7 @@ definePageMetadata(() => ({
 .userItemSub {
 	padding: 6px 12px;
 	font-size: 85%;
-	color: var(--fgTransparentWeak);
+	color: var(--MI_THEME-fgTransparentWeak);
 }
 
 .userItemMainBody {
diff --git a/packages/frontend/src/pages/admin/server-rules.vue b/packages/frontend/src/pages/admin/server-rules.vue
index ff9b8d6299..552958af6f 100644
--- a/packages/frontend/src/pages/admin/server-rules.vue
+++ b/packages/frontend/src/pages/admin/server-rules.vue
@@ -76,7 +76,7 @@ definePageMetadata(() => ({
 <style lang="scss" module>
 .item {
 	display: block;
-	color: var(--navFg);
+	color: var(--MI_THEME-navFg);
 }
 
 .itemHeader {
@@ -96,8 +96,8 @@ definePageMetadata(() => ({
 
 .itemNumber {
 	display: flex;
-	background-color: var(--accentedBg);
-	color: var(--accent);
+	background-color: var(--MI_THEME-accentedBg);
+	color: var(--MI_THEME-accent);
 	font-size: 14px;
 	font-weight: bold;
 	width: 28px;
@@ -117,12 +117,12 @@ definePageMetadata(() => ({
 .itemRemove {
 	width: 40px;
 	height: 40px;
-	color: var(--error);
+	color: var(--MI_THEME-error);
 	margin-left: auto;
 	border-radius: 6px;
 
 	&:hover {
-		background: var(--X5);
+		background: var(--MI_THEME-X5);
 	}
 }
 
diff --git a/packages/frontend/src/pages/admin/settings.vue b/packages/frontend/src/pages/admin/settings.vue
index 5a7cdee576..ea7603a45a 100644
--- a/packages/frontend/src/pages/admin/settings.vue
+++ b/packages/frontend/src/pages/admin/settings.vue
@@ -400,6 +400,6 @@ definePageMetadata(() => ({
 <style lang="scss" module>
 .subCaption {
 	font-size: 0.85em;
-	color: var(--fgTransparentWeak);
+	color: var(--MI_THEME-fgTransparentWeak);
 }
 </style>
diff --git a/packages/frontend/src/pages/admin/system-webhook.item.vue b/packages/frontend/src/pages/admin/system-webhook.item.vue
index 124790338c..45f0fff107 100644
--- a/packages/frontend/src/pages/admin/system-webhook.item.vue
+++ b/packages/frontend/src/pages/admin/system-webhook.item.vue
@@ -13,9 +13,9 @@ SPDX-License-Identifier: AGPL-3.0-only
 		<i
 			v-else-if="[200, 201, 204].includes(entity.latestStatus)"
 			class="ti ti-check"
-			:style="{ color: 'var(--success)' }"
+			:style="{ color: 'var(--MI_THEME-success)' }"
 		/>
-		<i v-else class="ti ti-alert-triangle" :style="{ color: 'var(--error)' }"/>
+		<i v-else class="ti ti-alert-triangle" :style="{ color: 'var(--MI_THEME-error)' }"/>
 	</template>
 	<template #suffix>
 		<MkTime v-if="entity.latestSentAt" :time="entity.latestSentAt" style="margin-right: 8px"/>
@@ -75,6 +75,6 @@ function onDeleteClick() {
 	margin-right: 0.75em;
 	flex-shrink: 0;
 	text-align: center;
-	color: var(--fgTransparentWeak);
+	color: var(--MI_THEME-fgTransparentWeak);
 }
 </style>
diff --git a/packages/frontend/src/pages/announcement.vue b/packages/frontend/src/pages/announcement.vue
index 802a6bf399..3840e6a494 100644
--- a/packages/frontend/src/pages/announcement.vue
+++ b/packages/frontend/src/pages/announcement.vue
@@ -20,9 +20,9 @@ SPDX-License-Identifier: AGPL-3.0-only
 					<span v-if="$i && !announcement.silence && !announcement.isRead" style="margin-right: 0.5em;">🆕</span>
 					<span style="margin-right: 0.5em;">
 						<i v-if="announcement.icon === 'info'" class="ti ti-info-circle"></i>
-						<i v-else-if="announcement.icon === 'warning'" class="ti ti-alert-triangle" style="color: var(--warn);"></i>
-						<i v-else-if="announcement.icon === 'error'" class="ti ti-circle-x" style="color: var(--error);"></i>
-						<i v-else-if="announcement.icon === 'success'" class="ti ti-check" style="color: var(--success);"></i>
+						<i v-else-if="announcement.icon === 'warning'" class="ti ti-alert-triangle" style="color: var(--MI_THEME-warn);"></i>
+						<i v-else-if="announcement.icon === 'error'" class="ti ti-circle-x" style="color: var(--MI_THEME-error);"></i>
+						<i v-else-if="announcement.icon === 'success'" class="ti ti-check" style="color: var(--MI_THEME-success);"></i>
 					</span>
 					<Mfm :text="announcement.title"/>
 				</div>
diff --git a/packages/frontend/src/pages/announcements.vue b/packages/frontend/src/pages/announcements.vue
index e50b208775..688a542988 100644
--- a/packages/frontend/src/pages/announcements.vue
+++ b/packages/frontend/src/pages/announcements.vue
@@ -17,9 +17,9 @@ SPDX-License-Identifier: AGPL-3.0-only
 							<span v-if="$i && !announcement.silence && !announcement.isRead" style="margin-right: 0.5em;">🆕</span>
 							<span style="margin-right: 0.5em;">
 								<i v-if="announcement.icon === 'info'" class="ti ti-info-circle"></i>
-								<i v-else-if="announcement.icon === 'warning'" class="ti ti-alert-triangle" style="color: var(--warn);"></i>
-								<i v-else-if="announcement.icon === 'error'" class="ti ti-circle-x" style="color: var(--error);"></i>
-								<i v-else-if="announcement.icon === 'success'" class="ti ti-check" style="color: var(--success);"></i>
+								<i v-else-if="announcement.icon === 'warning'" class="ti ti-alert-triangle" style="color: var(--MI_THEME-warn);"></i>
+								<i v-else-if="announcement.icon === 'error'" class="ti ti-circle-x" style="color: var(--MI_THEME-error);"></i>
+								<i v-else-if="announcement.icon === 'success'" class="ti ti-check" style="color: var(--MI_THEME-success);"></i>
 							</span>
 							<MkA :to="`/announcements/${announcement.id}`"><span>{{ announcement.title }}</span></MkA>
 						</div>
diff --git a/packages/frontend/src/pages/antenna-timeline.vue b/packages/frontend/src/pages/antenna-timeline.vue
index 22c5231dd9..167f402931 100644
--- a/packages/frontend/src/pages/antenna-timeline.vue
+++ b/packages/frontend/src/pages/antenna-timeline.vue
@@ -115,7 +115,7 @@ definePageMetadata(() => ({
 }
 
 .tl {
-	background: var(--bg);
+	background: var(--MI_THEME-bg);
 	border-radius: var(--radius);
 	overflow: clip;
 }
diff --git a/packages/frontend/src/pages/channel-editor.vue b/packages/frontend/src/pages/channel-editor.vue
index d3f4a65b89..6d8274a55c 100644
--- a/packages/frontend/src/pages/channel-editor.vue
+++ b/packages/frontend/src/pages/channel-editor.vue
@@ -216,7 +216,7 @@ definePageMetadata(() => ({
 	text-overflow: ellipsis;
 	overflow: hidden;
 	white-space: nowrap;
-	color: var(--navFg);
+	color: var(--MI_THEME-navFg);
 }
 
 .pinnedNoteRemove {
diff --git a/packages/frontend/src/pages/channel.vue b/packages/frontend/src/pages/channel.vue
index 8b014c7a4e..c8b04ca350 100644
--- a/packages/frontend/src/pages/channel.vue
+++ b/packages/frontend/src/pages/channel.vue
@@ -275,8 +275,8 @@ definePageMetadata(() => ({
 .footer {
 	-webkit-backdrop-filter: var(--blur, blur(15px));
 	backdrop-filter: var(--blur, blur(15px));
-	background: var(--acrylicBg);
-	border-top: solid 0.5px var(--divider);
+	background: var(--MI_THEME-acrylicBg);
+	border-top: solid 0.5px var(--MI_THEME-divider);
 }
 
 .bannerContainer {
@@ -310,7 +310,7 @@ definePageMetadata(() => ({
 	left: 0;
 	width: 100%;
 	height: 64px;
-	background: linear-gradient(0deg, var(--panel), color(from var(--panel) srgb r g b / 0));
+	background: linear-gradient(0deg, var(--MI_THEME-panel), color(from var(--MI_THEME-panel) srgb r g b / 0));
 }
 
 .bannerStatus {
@@ -335,7 +335,7 @@ definePageMetadata(() => ({
 	bottom: 16px;
 	left: 16px;
 	background: rgba(0, 0, 0, 0.7);
-	color: var(--warn);
+	color: var(--MI_THEME-warn);
 	border-radius: 6px;
 	font-weight: bold;
 	font-size: 1em;
diff --git a/packages/frontend/src/pages/clip.vue b/packages/frontend/src/pages/clip.vue
index 7e5f0423f6..7b1737fece 100644
--- a/packages/frontend/src/pages/clip.vue
+++ b/packages/frontend/src/pages/clip.vue
@@ -198,7 +198,7 @@ definePageMetadata(() => ({
 .user {
 	--height: 32px;
 	padding: 16px;
-	border-top: solid 0.5px var(--divider);
+	border-top: solid 0.5px var(--MI_THEME-divider);
 	line-height: var(--height);
 }
 
diff --git a/packages/frontend/src/pages/custom-emojis-manager.vue b/packages/frontend/src/pages/custom-emojis-manager.vue
index 4747aa5205..6d0b3d8d2e 100644
--- a/packages/frontend/src/pages/custom-emojis-manager.vue
+++ b/packages/frontend/src/pages/custom-emojis-manager.vue
@@ -331,14 +331,14 @@ definePageMetadata(() => ({
 				align-items: center;
 				padding: 11px;
 				text-align: left;
-				border: solid 1px var(--panel);
+				border: solid 1px var(--MI_THEME-panel);
 
 				&:hover {
-					border-color: var(--inputBorderHover);
+					border-color: var(--MI_THEME-inputBorderHover);
 				}
 
 				&.selected {
-					border-color: var(--accent);
+					border-color: var(--MI_THEME-accent);
 				}
 
 				> .img {
@@ -385,7 +385,7 @@ definePageMetadata(() => ({
 				text-align: left;
 
 				&:hover {
-					color: var(--accent);
+					color: var(--MI_THEME-accent);
 				}
 
 				> .img {
diff --git a/packages/frontend/src/pages/drive.file.info.vue b/packages/frontend/src/pages/drive.file.info.vue
index 12ebbbe3ff..98fa99e2a3 100644
--- a/packages/frontend/src/pages/drive.file.info.vue
+++ b/packages/frontend/src/pages/drive.file.info.vue
@@ -231,7 +231,7 @@ onMounted(async () => {
 <style lang="scss" module>
 
 .filePreviewRoot {
-	background: var(--panel);
+	background: var(--MI_THEME-panel);
 	border-radius: var(--radius);
 	// MkMediaList 内の上部マージン 4px
 	padding: calc(1rem - 4px) 1rem 1rem;
@@ -262,8 +262,8 @@ onMounted(async () => {
 
 		&:hover,
 		&:focus-visible {
-			background-color: var(--accentedBg);
-			color: var(--accent);
+			background-color: var(--MI_THEME-accentedBg);
+			color: var(--MI_THEME-accent);
 			text-decoration: none;
 			outline: none;
 		}
@@ -299,12 +299,12 @@ onMounted(async () => {
 	}
 
 	&:hover {
-		background-color: var(--accentedBg);
+		background-color: var(--MI_THEME-accentedBg);
 
 		>.fileName,
 		>.fileNameEditIcon {
 			visibility: visible;
-			color: var(--accent);
+			color: var(--MI_THEME-accent);
 		}
 	}
 }
@@ -332,11 +332,11 @@ onMounted(async () => {
 	}
 
 	&:hover {
-		color: var(--accent);
-		background-color: var(--accentedBg);
+		color: var(--MI_THEME-accent);
+		background-color: var(--MI_THEME-accentedBg);
 
 		.kvEditIcon {
-			color: var(--accent);
+			color: var(--MI_THEME-accent);
 			visibility: visible;
 		}
 	}
diff --git a/packages/frontend/src/pages/drop-and-fusion.game.vue b/packages/frontend/src/pages/drop-and-fusion.game.vue
index 4db952eac2..fb4d599c28 100644
--- a/packages/frontend/src/pages/drop-and-fusion.game.vue
+++ b/packages/frontend/src/pages/drop-and-fusion.game.vue
@@ -111,7 +111,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 			<div v-if="replaying" class="_woodenFrame">
 				<div class="_woodenFrameInner">
 					<div style="background: #0004;">
-						<div style="height: 10px; background: var(--accent); will-change: width;" :style="{ width: `${(currentFrame / endedAtFrame) * 100}%` }"></div>
+						<div style="height: 10px; background: var(--MI_THEME-accent); will-change: width;" :style="{ width: `${(currentFrame / endedAtFrame) * 100}%` }"></div>
 					</div>
 				</div>
 				<div class="_woodenFrameInner">
diff --git a/packages/frontend/src/pages/emoji-edit-dialog.vue b/packages/frontend/src/pages/emoji-edit-dialog.vue
index 853c1d6b0b..bd798d9f3a 100644
--- a/packages/frontend/src/pages/emoji-edit-dialog.vue
+++ b/packages/frontend/src/pages/emoji-edit-dialog.vue
@@ -243,8 +243,8 @@ async function del() {
 	bottom: 0;
 	left: 0;
 	padding: 12px;
-	border-top: solid 0.5px var(--divider);
-	background: var(--acrylicBg);
+	border-top: solid 0.5px var(--MI_THEME-divider);
+	background: var(--MI_THEME-acrylicBg);
 	-webkit-backdrop-filter: var(--blur, blur(15px));
 	backdrop-filter: var(--blur, blur(15px));
 }
diff --git a/packages/frontend/src/pages/emojis.emoji.vue b/packages/frontend/src/pages/emojis.emoji.vue
index 97429c29a4..fcd22155b7 100644
--- a/packages/frontend/src/pages/emojis.emoji.vue
+++ b/packages/frontend/src/pages/emojis.emoji.vue
@@ -58,11 +58,11 @@ function menu(ev) {
 	align-items: center;
 	padding: 12px;
 	text-align: left;
-	background: var(--panel);
+	background: var(--MI_THEME-panel);
 	border-radius: 8px;
 
 	&:hover {
-		border-color: var(--accent);
+		border-color: var(--MI_THEME-accent);
 	}
 }
 
diff --git a/packages/frontend/src/pages/favorites.vue b/packages/frontend/src/pages/favorites.vue
index c3d4cae4aa..e2765da3e9 100644
--- a/packages/frontend/src/pages/favorites.vue
+++ b/packages/frontend/src/pages/favorites.vue
@@ -46,7 +46,7 @@ definePageMetadata(() => ({
 
 <style lang="scss" module>
 .note {
-	background: var(--panel);
+	background: var(--MI_THEME-panel);
 	border-radius: var(--radius);
 }
 </style>
diff --git a/packages/frontend/src/pages/flash/flash-edit.vue b/packages/frontend/src/pages/flash/flash-edit.vue
index fd6fadd0b3..87bd707f6d 100644
--- a/packages/frontend/src/pages/flash/flash-edit.vue
+++ b/packages/frontend/src/pages/flash/flash-edit.vue
@@ -468,7 +468,7 @@ definePageMetadata(() => ({
 <style lang="scss" module>
 .footer {
 	backdrop-filter: var(--blur, blur(15px));
-	background: var(--acrylicBg);
-	border-top: solid .5px var(--divider);
+	background: var(--MI_THEME-acrylicBg);
+	border-top: solid .5px var(--MI_THEME-divider);
 }
 </style>
diff --git a/packages/frontend/src/pages/flash/flash.vue b/packages/frontend/src/pages/flash/flash.vue
index cf10bee0f5..3b982a405e 100644
--- a/packages/frontend/src/pages/flash/flash.vue
+++ b/packages/frontend/src/pages/flash/flash.vue
@@ -51,7 +51,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 						<div><i class="ti ti-clock"></i> {{ i18n.ts.createdAt }}: <MkTime :time="flash.createdAt" mode="detail"/></div>
 					</div>
 				</div>
-				<MkA v-if="$i && $i.id === flash.userId" :to="`/play/${flash.id}/edit`" style="color: var(--accent);">{{ i18n.ts._play.editThisPage }}</MkA>
+				<MkA v-if="$i && $i.id === flash.userId" :to="`/play/${flash.id}/edit`" style="color: var(--MI_THEME-accent);">{{ i18n.ts._play.editThisPage }}</MkA>
 				<MkAd :prefer="['horizontal', 'horizontal-big']"/>
 			</div>
 			<MkError v-else-if="error" @retry="fetchFlash()"/>
@@ -367,7 +367,7 @@ definePageMetadata(() => ({
 				justify-content: center;
 				gap: 12px;
 				padding: 16px;
-				border-bottom: 1px solid var(--divider);
+				border-bottom: 1px solid var(--MI_THEME-divider);
 
 				&:last-child {
 					border-bottom: none;
diff --git a/packages/frontend/src/pages/gallery/edit.vue b/packages/frontend/src/pages/gallery/edit.vue
index a68a7e5c41..70f8b2c31d 100644
--- a/packages/frontend/src/pages/gallery/edit.vue
+++ b/packages/frontend/src/pages/gallery/edit.vue
@@ -141,7 +141,7 @@ definePageMetadata(() => ({
 		top: 8px;
 		left: 9px;
 		padding: 8px;
-		background: var(--panel);
+		background: var(--MI_THEME-panel);
 	}
 
 	> .remove {
@@ -149,7 +149,7 @@ definePageMetadata(() => ({
 		top: 8px;
 		right: 9px;
 		padding: 8px;
-		background: var(--panel);
+		background: var(--MI_THEME-panel);
 	}
 }
 </style>
diff --git a/packages/frontend/src/pages/gallery/post.vue b/packages/frontend/src/pages/gallery/post.vue
index 8c4dfc3b83..aab4e53454 100644
--- a/packages/frontend/src/pages/gallery/post.vue
+++ b/packages/frontend/src/pages/gallery/post.vue
@@ -262,14 +262,14 @@ definePageMetadata(() => ({
 			align-items: center;
 			margin-top: 16px;
 			padding: 16px 0 0 0;
-			border-top: solid 0.5px var(--divider);
+			border-top: solid 0.5px var(--MI_THEME-divider);
 
 			> .like {
 				> .button {
-					--accent: rgb(241 97 132);
-					--X8: rgb(241 92 128);
-					--buttonBg: rgb(216 71 106 / 5%);
-					--buttonHoverBg: rgb(216 71 106 / 10%);
+					--MI_THEME-accent: rgb(241 97 132);
+					--MI_THEME-X8: rgb(241 92 128);
+					--MI_THEME-buttonBg: rgb(216 71 106 / 5%);
+					--MI_THEME-buttonHoverBg: rgb(216 71 106 / 10%);
 					color: #ff002f;
 
 					::v-deep(.count) {
@@ -286,7 +286,7 @@ definePageMetadata(() => ({
 					margin: 0 8px;
 
 					&:hover {
-						color: var(--fgHighlighted);
+						color: var(--MI_THEME-fgHighlighted);
 					}
 				}
 			}
@@ -295,7 +295,7 @@ definePageMetadata(() => ({
 		> .user {
 			margin-top: 16px;
 			padding: 16px 0 0 0;
-			border-top: solid 0.5px var(--divider);
+			border-top: solid 0.5px var(--MI_THEME-divider);
 			display: flex;
 			align-items: center;
 			flex-wrap: wrap;
diff --git a/packages/frontend/src/pages/games.vue b/packages/frontend/src/pages/games.vue
index b52f4decaa..998b8be0f3 100644
--- a/packages/frontend/src/pages/games.vue
+++ b/packages/frontend/src/pages/games.vue
@@ -35,7 +35,7 @@ definePageMetadata(() => ({
 
 <style module>
 .link:focus-within {
-	outline: 2px solid var(--focus);
+	outline: 2px solid var(--MI_THEME-focus);
 	outline-offset: -2px;
 }
 </style>
diff --git a/packages/frontend/src/pages/install-extensions.vue b/packages/frontend/src/pages/install-extensions.vue
index 83f16fce68..30e658d8c0 100644
--- a/packages/frontend/src/pages/install-extensions.vue
+++ b/packages/frontend/src/pages/install-extensions.vue
@@ -21,7 +21,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 							<template #key>{{ i18n.ts._externalResourceInstaller._vendorInfo.hashVerify }}</template>
 							<template #value>
 								<!-- この画面が出ている時点でハッシュの検証には成功している -->
-								<i class="ti ti-check" style="color: var(--accent)"></i>
+								<i class="ti ti-check" style="color: var(--MI_THEME-accent)"></i>
 							</template>
 						</MkKeyValue>
 					</div>
@@ -251,7 +251,7 @@ definePageMetadata(() => ({
 <style lang="scss" module>
 .extInstallerRoot {
 	border-radius: var(--radius);
-	background: var(--panel);
+	background: var(--MI_THEME-panel);
 	padding: 1.5rem;
 }
 
@@ -265,8 +265,8 @@ definePageMetadata(() => ({
 	margin-left: auto;
 	margin-right: auto;
 
-	background-color: var(--accentedBg);
-	color: var(--accent);
+	background-color: var(--MI_THEME-accentedBg);
+	color: var(--MI_THEME-accent);
 }
 
 .error .extInstallerIconWrapper {
diff --git a/packages/frontend/src/pages/my-antennas/index.vue b/packages/frontend/src/pages/my-antennas/index.vue
index 21c96348f0..f387740728 100644
--- a/packages/frontend/src/pages/my-antennas/index.vue
+++ b/packages/frontend/src/pages/my-antennas/index.vue
@@ -73,11 +73,11 @@ onActivated(() => {
 .antenna {
 	display: block;
 	padding: 16px;
-	border: solid 1px var(--divider);
+	border: solid 1px var(--MI_THEME-divider);
 	border-radius: 6px;
 
 	&:hover {
-		border: solid 1px var(--accent);
+		border: solid 1px var(--MI_THEME-accent);
 		text-decoration: none;
 	}
 }
diff --git a/packages/frontend/src/pages/my-lists/index.vue b/packages/frontend/src/pages/my-lists/index.vue
index 82fde284c1..6cbcca73c2 100644
--- a/packages/frontend/src/pages/my-lists/index.vue
+++ b/packages/frontend/src/pages/my-lists/index.vue
@@ -85,12 +85,12 @@ onActivated(() => {
 .list {
 	display: block;
 	padding: 16px;
-	border: solid 1px var(--divider);
+	border: solid 1px var(--MI_THEME-divider);
 	border-radius: 6px;
 	margin-bottom: 8px;
 
 	&:hover {
-		border: solid 1px var(--accent);
+		border: solid 1px var(--MI_THEME-accent);
 		text-decoration: none;
 	}
 }
diff --git a/packages/frontend/src/pages/my-lists/list.vue b/packages/frontend/src/pages/my-lists/list.vue
index 5f195693cc..a78f4bb539 100644
--- a/packages/frontend/src/pages/my-lists/list.vue
+++ b/packages/frontend/src/pages/my-lists/list.vue
@@ -134,7 +134,7 @@ async function removeUser(item, ev) {
 
 async function showMembershipMenu(item, ev) {
 	const withRepliesRef = ref(item.withReplies);
-	
+
 	os.popupMenu([{
 		type: 'switch',
 		text: i18n.ts.showRepliesToOthersInTimeline,
@@ -236,6 +236,6 @@ definePageMetadata(() => ({
 .footer {
 	-webkit-backdrop-filter: var(--blur, blur(15px));
 	backdrop-filter: var(--blur, blur(15px));
-	border-top: solid 0.5px var(--divider);
+	border-top: solid 0.5px var(--MI_THEME-divider);
 }
 </style>
diff --git a/packages/frontend/src/pages/note.vue b/packages/frontend/src/pages/note.vue
index 97f32d35cd..d2e7559109 100644
--- a/packages/frontend/src/pages/note.vue
+++ b/packages/frontend/src/pages/note.vue
@@ -183,6 +183,6 @@ definePageMetadata(() => ({
 
 .note {
 	border-radius: var(--radius);
-	background: var(--panel);
+	background: var(--MI_THEME-panel);
 }
 </style>
diff --git a/packages/frontend/src/pages/page-editor/els/page-editor.el.text.vue b/packages/frontend/src/pages/page-editor/els/page-editor.el.text.vue
index 14c3e6845e..f09f7e1acd 100644
--- a/packages/frontend/src/pages/page-editor/els/page-editor.el.text.vue
+++ b/packages/frontend/src/pages/page-editor/els/page-editor.el.text.vue
@@ -63,7 +63,7 @@ onUnmounted(() => {
 	box-shadow: none;
 	padding: 16px;
 	background: transparent;
-	color: var(--fg);
+	color: var(--MI_THEME-fg);
 	font-size: 14px;
 	box-sizing: border-box;
 }
diff --git a/packages/frontend/src/pages/page-editor/page-editor.container.vue b/packages/frontend/src/pages/page-editor/page-editor.container.vue
index f2081c452c..a96c2c2a77 100644
--- a/packages/frontend/src/pages/page-editor/page-editor.container.vue
+++ b/packages/frontend/src/pages/page-editor/page-editor.container.vue
@@ -60,12 +60,12 @@ function remove() {
 .cpjygsrt {
 	position: relative;
 	overflow: hidden;
-	background: var(--panel);
-	border: solid 2px var(--X12);
+	background: var(--MI_THEME-panel);
+	border: solid 2px var(--MI_THEME-X12);
 	border-radius: 8px;
 
 	&:hover {
-		border: solid 2px var(--X13);
+		border: solid 2px var(--MI_THEME-X13);
 	}
 
 	&.warn {
diff --git a/packages/frontend/src/pages/page.vue b/packages/frontend/src/pages/page.vue
index 7926dab88b..73fe938e9c 100644
--- a/packages/frontend/src/pages/page.vue
+++ b/packages/frontend/src/pages/page.vue
@@ -357,8 +357,8 @@ definePageMetadata(() => ({
 
 	&:hover,
 	&:focus-visible {
-		background-color: var(--accentedBg);
-		color: var(--accent);
+		background-color: var(--MI_THEME-accentedBg);
+		color: var(--MI_THEME-accent);
 		text-decoration: none;
 		outline: none;
 	}
@@ -367,7 +367,7 @@ definePageMetadata(() => ({
 .pageMain {
 	border-radius: var(--radius);
 	padding: 2rem;
-	background: var(--panel);
+	background: var(--MI_THEME-panel);
 	box-sizing: border-box;
 }
 
@@ -399,7 +399,7 @@ definePageMetadata(() => ({
 		}
 
 		.pageBannerBgFallback2 {
-			background-color: var(--accentedBg);
+			background-color: var(--MI_THEME-accentedBg);
 		}
 
 		&::after {
@@ -409,7 +409,7 @@ definePageMetadata(() => ({
 			bottom: 0;
 			width: 100%;
 			height: 100px;
-			background: linear-gradient(0deg, var(--panel), transparent);
+			background: linear-gradient(0deg, var(--MI_THEME-panel), transparent);
 		}
 	}
 
@@ -433,7 +433,7 @@ definePageMetadata(() => ({
 		h1 {
 			font-size: 2rem;
 			font-weight: 700;
-			color: var(--fg);
+			color: var(--MI_THEME-fg);
 			margin: 0;
 		}
 
@@ -472,7 +472,7 @@ definePageMetadata(() => ({
 	display: flex;
 	align-items: center;
 
-	border-top: 1px solid var(--divider);
+	border-top: 1px solid var(--MI_THEME-divider);
 	padding-top: 1.5rem;
 	margin-bottom: 1.5rem;
 
@@ -487,7 +487,7 @@ definePageMetadata(() => ({
 	display: flex;
 	align-items: center;
 
-	border-top: 1px solid var(--divider);
+	border-top: 1px solid var(--MI_THEME-divider);
 	padding-top: 1.5rem;
 	margin-bottom: 1.5rem;
 
@@ -534,6 +534,6 @@ definePageMetadata(() => ({
 }
 
 .relatedPagesItem > article {
-	background-color: var(--panelHighlight) !important;
+	background-color: var(--MI_THEME-panelHighlight) !important;
 }
 </style>
diff --git a/packages/frontend/src/pages/reversi/game.board.vue b/packages/frontend/src/pages/reversi/game.board.vue
index 54e66f6e16..429f502133 100644
--- a/packages/frontend/src/pages/reversi/game.board.vue
+++ b/packages/frontend/src/pages/reversi/game.board.vue
@@ -504,7 +504,7 @@ $gap: 4px;
 .boardInner {
 	padding: 32px;
 
-	background: var(--panel);
+	background: var(--MI_THEME-panel);
 	box-shadow: 0 0 2px 1px #ce8a5c, inset 0 0 1px 1px #693410;
 	border-radius: 8px;
 }
@@ -574,34 +574,34 @@ $gap: 4px;
 	transition: border 0.25s ease, opacity 0.25s ease;
 
 	&.boardCell_empty {
-		border: solid 2px var(--divider);
+		border: solid 2px var(--MI_THEME-divider);
 	}
 
 	&.boardCell_empty.boardCell_can {
-		border-color: var(--accent);
+		border-color: var(--MI_THEME-accent);
 		opacity: 0.5;
 	}
 
 	&.boardCell_empty.boardCell_myTurn {
-		border-color: var(--divider);
+		border-color: var(--MI_THEME-divider);
 		opacity: 1;
 
 		&.boardCell_can {
-			border-color: var(--accent);
+			border-color: var(--MI_THEME-accent);
 			cursor: pointer;
 
 			&:hover {
-				background: var(--accent);
+				background: var(--MI_THEME-accent);
 			}
 		}
 	}
 
 	&.boardCell_prev {
-		box-shadow: 0 0 0 4px var(--accent);
+		box-shadow: 0 0 0 4px var(--MI_THEME-accent);
 	}
 
 	&.boardCell_isEnded {
-		border-color: var(--divider);
+		border-color: var(--MI_THEME-divider);
 	}
 
 	&.boardCell_none {
diff --git a/packages/frontend/src/pages/reversi/game.setting.vue b/packages/frontend/src/pages/reversi/game.setting.vue
index 08bb3cb76c..f24614f2eb 100644
--- a/packages/frontend/src/pages/reversi/game.setting.vue
+++ b/packages/frontend/src/pages/reversi/game.setting.vue
@@ -17,7 +17,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 				</template>
 				<template v-else>
 					<div class="_panel">
-						<div style="display: flex; align-items: center; padding: 16px; border-bottom: solid 1px var(--divider);">
+						<div style="display: flex; align-items: center; padding: 16px; border-bottom: solid 1px var(--MI_THEME-divider);">
 							<div>{{ mapName }}</div>
 							<MkButton style="margin-left: auto;" @click="chooseMap">{{ i18n.ts._reversi.chooseBoard }}</MkButton>
 						</div>
@@ -87,7 +87,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 		<div :class="$style.footer">
 			<MkSpacer :contentMax="700" :marginMin="16" :marginMax="16">
 				<div style="text-align: center;" class="_gaps_s">
-					<div v-if="opponentHasSettingsChanged" style="color: var(--warn);">{{ i18n.ts._reversi.opponentHasSettingsChanged }}</div>
+					<div v-if="opponentHasSettingsChanged" style="color: var(--MI_THEME-warn);">{{ i18n.ts._reversi.opponentHasSettingsChanged }}</div>
 					<div>
 						<template v-if="isReady && isOpReady">{{ i18n.ts._reversi.thisGameIsStartedSoon }}<MkEllipsis/></template>
 						<template v-if="isReady && !isOpReady">{{ i18n.ts._reversi.waitingForOther }}<MkEllipsis/></template>
@@ -273,14 +273,14 @@ onUnmounted(() => {
 	width: 300px;
 	height: 300px;
 	margin: 0 auto;
-	color: var(--fg);
+	color: var(--MI_THEME-fg);
 }
 
 .boardCell {
 	display: grid;
 	place-items: center;
 	background: transparent;
-	border: solid 2px var(--divider);
+	border: solid 2px var(--MI_THEME-divider);
 	border-radius: 6px;
 	overflow: clip;
 	cursor: pointer;
@@ -292,7 +292,7 @@ onUnmounted(() => {
 .footer {
 	-webkit-backdrop-filter: var(--blur, blur(15px));
 	backdrop-filter: var(--blur, blur(15px));
-	background: var(--acrylicBg);
-	border-top: solid 0.5px var(--divider);
+	background: var(--MI_THEME-acrylicBg);
+	border-top: solid 0.5px var(--MI_THEME-divider);
 }
 </style>
diff --git a/packages/frontend/src/pages/reversi/index.vue b/packages/frontend/src/pages/reversi/index.vue
index d823861b4a..91616d3a50 100644
--- a/packages/frontend/src/pages/reversi/index.vue
+++ b/packages/frontend/src/pages/reversi/index.vue
@@ -36,13 +36,13 @@ SPDX-License-Identifier: AGPL-3.0-only
 					<div :class="$style.gamePreviews">
 						<MkA v-for="g in items" :key="g.id" v-panel :class="[$style.gamePreview, !g.isStarted && !g.isEnded && $style.gamePreviewWaiting, g.isStarted && !g.isEnded && $style.gamePreviewActive]" tabindex="-1" :to="`/reversi/g/${g.id}`">
 							<div :class="$style.gamePreviewPlayers">
-								<span v-if="g.winnerId === g.user1Id" style="margin-right: 0.75em; color: var(--accent); font-weight: bold;"><i class="ti ti-trophy"></i></span>
+								<span v-if="g.winnerId === g.user1Id" style="margin-right: 0.75em; color: var(--MI_THEME-accent); font-weight: bold;"><i class="ti ti-trophy"></i></span>
 								<span v-if="g.winnerId === g.user2Id" style="margin-right: 0.75em; visibility: hidden;"><i class="ti ti-x"></i></span>
 								<MkAvatar :class="$style.gamePreviewPlayersAvatar" :user="g.user1"/>
 								<span style="margin: 0 1em;">vs</span>
 								<MkAvatar :class="$style.gamePreviewPlayersAvatar" :user="g.user2"/>
 								<span v-if="g.winnerId === g.user1Id" style="margin-left: 0.75em; visibility: hidden;"><i class="ti ti-x"></i></span>
-								<span v-if="g.winnerId === g.user2Id" style="margin-left: 0.75em; color: var(--accent); font-weight: bold;"><i class="ti ti-trophy"></i></span>
+								<span v-if="g.winnerId === g.user2Id" style="margin-left: 0.75em; color: var(--MI_THEME-accent); font-weight: bold;"><i class="ti ti-trophy"></i></span>
 							</div>
 							<div :class="$style.gamePreviewFooter">
 								<span v-if="g.isStarted && !g.isEnded" :class="$style.gamePreviewStatusActive">{{ i18n.ts._reversi.playing }}</span>
@@ -63,13 +63,13 @@ SPDX-License-Identifier: AGPL-3.0-only
 					<div :class="$style.gamePreviews">
 						<MkA v-for="g in items" :key="g.id" v-panel :class="[$style.gamePreview, !g.isStarted && !g.isEnded && $style.gamePreviewWaiting, g.isStarted && !g.isEnded && $style.gamePreviewActive]" tabindex="-1" :to="`/reversi/g/${g.id}`">
 							<div :class="$style.gamePreviewPlayers">
-								<span v-if="g.winnerId === g.user1Id" style="margin-right: 0.75em; color: var(--accent); font-weight: bold;"><i class="ti ti-trophy"></i></span>
+								<span v-if="g.winnerId === g.user1Id" style="margin-right: 0.75em; color: var(--MI_THEME-accent); font-weight: bold;"><i class="ti ti-trophy"></i></span>
 								<span v-if="g.winnerId === g.user2Id" style="margin-right: 0.75em; visibility: hidden;"><i class="ti ti-x"></i></span>
 								<MkAvatar :class="$style.gamePreviewPlayersAvatar" :user="g.user1"/>
 								<span style="margin: 0 1em;">vs</span>
 								<MkAvatar :class="$style.gamePreviewPlayersAvatar" :user="g.user2"/>
 								<span v-if="g.winnerId === g.user1Id" style="margin-left: 0.75em; visibility: hidden;"><i class="ti ti-x"></i></span>
-								<span v-if="g.winnerId === g.user2Id" style="margin-left: 0.75em; color: var(--accent); font-weight: bold;"><i class="ti ti-trophy"></i></span>
+								<span v-if="g.winnerId === g.user2Id" style="margin-left: 0.75em; color: var(--MI_THEME-accent); font-weight: bold;"><i class="ti ti-trophy"></i></span>
 							</div>
 							<div :class="$style.gamePreviewFooter">
 								<span v-if="g.isStarted && !g.isEnded" :class="$style.gamePreviewStatusActive">{{ i18n.ts._reversi.playing }}</span>
@@ -295,11 +295,11 @@ definePageMetadata(() => ({
 }
 
 .gamePreviewActive {
-	box-shadow: inset 0 0 8px 0px var(--accent);
+	box-shadow: inset 0 0 8px 0px var(--MI_THEME-accent);
 }
 
 .gamePreviewWaiting {
-	box-shadow: inset 0 0 8px 0px var(--warn);
+	box-shadow: inset 0 0 8px 0px var(--MI_THEME-warn);
 }
 
 .gamePreviewPlayers {
@@ -324,19 +324,19 @@ definePageMetadata(() => ({
 .gamePreviewFooter {
 	display: flex;
 	align-items: baseline;
-	border-top: solid 0.5px var(--divider);
+	border-top: solid 0.5px var(--MI_THEME-divider);
 	padding: 6px 10px;
 	font-size: 0.9em;
 }
 
 .gamePreviewStatusActive {
-	color: var(--accent);
+	color: var(--MI_THEME-accent);
 	font-weight: bold;
 	animation: blink 2s infinite;
 }
 
 .gamePreviewStatusWaiting {
-	color: var(--warn);
+	color: var(--MI_THEME-warn);
 	font-weight: bold;
 	animation: blink 2s infinite;
 }
diff --git a/packages/frontend/src/pages/scratchpad.vue b/packages/frontend/src/pages/scratchpad.vue
index 155d8b82d7..280a8d0d44 100644
--- a/packages/frontend/src/pages/scratchpad.vue
+++ b/packages/frontend/src/pages/scratchpad.vue
@@ -242,14 +242,14 @@ definePageMetadata(() => ({
 }
 
 .uiInspectorUnShown {
-	color: var(--fgTransparent);
+	color: var(--MI_THEME-fgTransparent);
 }
 
 .uiInspectorType {
 	display: inline-block;
 	border: hidden;
 	border-radius: 10px;
-	background-color: var(--panelHighlight);
+	background-color: var(--MI_THEME-panelHighlight);
 	padding: 2px 8px;
 	font-size: 12px;
 }
diff --git a/packages/frontend/src/pages/search.note.vue b/packages/frontend/src/pages/search.note.vue
index 9cf7fbe8d8..105c947d25 100644
--- a/packages/frontend/src/pages/search.note.vue
+++ b/packages/frontend/src/pages/search.note.vue
@@ -211,12 +211,12 @@ async function search() {
 	justify-content: center;
 }
 .addMeButton {
-  border: 2px dashed var(--fgTransparent);
+  border: 2px dashed var(--MI_THEME-fgTransparent);
 	padding: 12px;
 	margin-right: 16px;
 }
 .addUserButton {
-  border: 2px dashed var(--fgTransparent);
+  border: 2px dashed var(--MI_THEME-fgTransparent);
 	padding: 12px;
 	flex-grow: 1;
 }
diff --git a/packages/frontend/src/pages/settings/2fa.vue b/packages/frontend/src/pages/settings/2fa.vue
index 6a9a1e16e2..a76b748ac1 100644
--- a/packages/frontend/src/pages/settings/2fa.vue
+++ b/packages/frontend/src/pages/settings/2fa.vue
@@ -19,7 +19,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 			<template #icon><i class="ti ti-shield-lock"></i></template>
 			<template #label>{{ i18n.ts.totp }}</template>
 			<template #caption>{{ i18n.ts.totpDescription }}</template>
-			<template #suffix><i v-if="$i.twoFactorEnabled" class="ti ti-check" style="color: var(--success)"></i></template>
+			<template #suffix><i v-if="$i.twoFactorEnabled" class="ti ti-check" style="color: var(--MI_THEME-success)"></i></template>
 
 			<div v-if="$i.twoFactorEnabled" class="_gaps_s">
 				<div v-text="i18n.ts._2fa.alreadyRegistered"/>
diff --git a/packages/frontend/src/pages/settings/avatar-decoration.decoration.vue b/packages/frontend/src/pages/settings/avatar-decoration.decoration.vue
index f37b53aebb..f72a0b9383 100644
--- a/packages/frontend/src/pages/settings/avatar-decoration.decoration.vue
+++ b/packages/frontend/src/pages/settings/avatar-decoration.decoration.vue
@@ -43,7 +43,7 @@ const emit = defineEmits<{
 .root {
 	cursor: pointer;
 	padding: 16px 16px 28px 16px;
-	border: solid 2px var(--divider);
+	border: solid 2px var(--MI_THEME-divider);
 	border-radius: 8px;
 	text-align: center;
 	font-size: 90%;
@@ -52,8 +52,8 @@ const emit = defineEmits<{
 }
 
 .active {
-	background-color: var(--accentedBg);
-	border-color: var(--accent);
+	background-color: var(--MI_THEME-accentedBg);
+	border-color: var(--MI_THEME-accent);
 }
 
 .name {
diff --git a/packages/frontend/src/pages/settings/avatar-decoration.dialog.vue b/packages/frontend/src/pages/settings/avatar-decoration.dialog.vue
index 1938b8d48d..7f1c6fd401 100644
--- a/packages/frontend/src/pages/settings/avatar-decoration.dialog.vue
+++ b/packages/frontend/src/pages/settings/avatar-decoration.dialog.vue
@@ -150,7 +150,7 @@ async function detach() {
 	bottom: 0;
 	left: 0;
 	padding: 12px;
-	border-top: solid 0.5px var(--divider);
+	border-top: solid 0.5px var(--MI_THEME-divider);
 	-webkit-backdrop-filter: var(--blur, blur(15px));
 	backdrop-filter: var(--blur, blur(15px));
 }
diff --git a/packages/frontend/src/pages/settings/drive-cleaner.vue b/packages/frontend/src/pages/settings/drive-cleaner.vue
index 8d2946db63..6a13984dd7 100644
--- a/packages/frontend/src/pages/settings/drive-cleaner.vue
+++ b/packages/frontend/src/pages/settings/drive-cleaner.vue
@@ -132,7 +132,7 @@ definePageMetadata(() => ({
 	align-items: center;
 
 	&:hover {
-		color: var(--accent);
+		color: var(--MI_THEME-accent);
 	}
 }
 
diff --git a/packages/frontend/src/pages/settings/email.vue b/packages/frontend/src/pages/settings/email.vue
index f226647569..d452f249b6 100644
--- a/packages/frontend/src/pages/settings/email.vue
+++ b/packages/frontend/src/pages/settings/email.vue
@@ -10,7 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 		<MkInput v-model="emailAddress" type="email" manualSave>
 			<template #prefix><i class="ti ti-mail"></i></template>
 			<template v-if="$i.email && !$i.emailVerified" #caption>{{ i18n.ts.verificationEmailSent }}</template>
-			<template v-else-if="emailAddress === $i.email && $i.emailVerified" #caption><i class="ti ti-check" style="color: var(--success);"></i> {{ i18n.ts.emailVerified }}</template>
+			<template v-else-if="emailAddress === $i.email && $i.emailVerified" #caption><i class="ti ti-check" style="color: var(--MI_THEME-success);"></i> {{ i18n.ts.emailVerified }}</template>
 		</MkInput>
 	</FormSection>
 
diff --git a/packages/frontend/src/pages/settings/emoji-picker.vue b/packages/frontend/src/pages/settings/emoji-picker.vue
index 999a73df4c..427cdbe64e 100644
--- a/packages/frontend/src/pages/settings/emoji-picker.vue
+++ b/packages/frontend/src/pages/settings/emoji-picker.vue
@@ -250,7 +250,7 @@ definePageMetadata(() => ({
 .tab {
 	margin: calc(var(--margin) / 2) 0;
 	padding: calc(var(--margin) / 2) 0;
-	background: var(--bg);
+	background: var(--MI_THEME-bg);
 }
 
 .emojis {
@@ -272,6 +272,6 @@ definePageMetadata(() => ({
 .editorCaption {
 	font-size: 0.85em;
 	padding: 8px 0 0 0;
-	color: var(--fgTransparentWeak);
+	color: var(--MI_THEME-fgTransparentWeak);
 }
 </style>
diff --git a/packages/frontend/src/pages/settings/mute-block.vue b/packages/frontend/src/pages/settings/mute-block.vue
index f4ee7dffbf..4d413d53ab 100644
--- a/packages/frontend/src/pages/settings/mute-block.vue
+++ b/packages/frontend/src/pages/settings/mute-block.vue
@@ -244,7 +244,7 @@ definePageMetadata(() => ({
 .userItemSub {
 	padding: 6px 12px;
 	font-size: 85%;
-	color: var(--fgTransparentWeak);
+	color: var(--MI_THEME-fgTransparentWeak);
 }
 
 .userItemMainBody {
diff --git a/packages/frontend/src/pages/settings/navbar.vue b/packages/frontend/src/pages/settings/navbar.vue
index a0e6cad9c8..b189db0f8f 100644
--- a/packages/frontend/src/pages/settings/navbar.vue
+++ b/packages/frontend/src/pages/settings/navbar.vue
@@ -122,7 +122,7 @@ definePageMetadata(() => ({
 	text-overflow: ellipsis;
 	overflow: hidden;
 	white-space: nowrap;
-	color: var(--navFg);
+	color: var(--MI_THEME-navFg);
 }
 
 .itemIcon {
diff --git a/packages/frontend/src/pages/settings/profile.vue b/packages/frontend/src/pages/settings/profile.vue
index 19c5d892de..0d61f8d851 100644
--- a/packages/frontend/src/pages/settings/profile.vue
+++ b/packages/frontend/src/pages/settings/profile.vue
@@ -282,7 +282,7 @@ definePageMetadata(() => ({
 	height: 130px;
 	background-size: cover;
 	background-position: center;
-	border-bottom: solid 1px var(--divider);
+	border-bottom: solid 1px var(--MI_THEME-divider);
 	overflow: clip;
 }
 
diff --git a/packages/frontend/src/pages/settings/security.vue b/packages/frontend/src/pages/settings/security.vue
index de0f63630e..8f9d4f858b 100644
--- a/packages/frontend/src/pages/settings/security.vue
+++ b/packages/frontend/src/pages/settings/security.vue
@@ -124,7 +124,7 @@ definePageMetadata(() => ({
 	}
 
 	&:not(:last-child) {
-		border-bottom: solid 0.5px var(--divider);
+		border-bottom: solid 0.5px var(--MI_THEME-divider);
 	}
 
 	> header {
@@ -136,11 +136,11 @@ definePageMetadata(() => ({
 			margin-right: 0.75em;
 
 			&.succ {
-				color: var(--success);
+				color: var(--MI_THEME-success);
 			}
 
 			&.fail {
-				color: var(--error);
+				color: var(--MI_THEME-error);
 			}
 		}
 
diff --git a/packages/frontend/src/pages/settings/sounds.sound.vue b/packages/frontend/src/pages/settings/sounds.sound.vue
index 81478fede5..56f65e2309 100644
--- a/packages/frontend/src/pages/settings/sounds.sound.vue
+++ b/packages/frontend/src/pages/settings/sounds.sound.vue
@@ -194,7 +194,7 @@ function save() {
 	flex-grow: 1;
 	min-width: 0;
 	font-weight: 700;
-	color: var(--error);
+	color: var(--MI_THEME-error);
 }
 
 .fileSelectorButton {
@@ -203,6 +203,6 @@ function save() {
 
 .fileNotSelected {
 	font-weight: 700;
-	color: var(--infoWarnFg);
+	color: var(--MI_THEME-infoWarnFg);
 }
 </style>
diff --git a/packages/frontend/src/pages/settings/theme.vue b/packages/frontend/src/pages/settings/theme.vue
index ce8ec68692..73cc075082 100644
--- a/packages/frontend/src/pages/settings/theme.vue
+++ b/packages/frontend/src/pages/settings/theme.vue
@@ -204,7 +204,7 @@ definePageMetadata(() => ({
 		}
 
 		.dn:focus-visible ~ .toggle {
-			outline: 2px solid var(--focus);
+			outline: 2px solid var(--MI_THEME-focus);
 			outline-offset: 2px;
 		}
 
@@ -227,12 +227,12 @@ definePageMetadata(() => ({
 
 			> .before {
 				left: -70px;
-				color: var(--accent);
+				color: var(--MI_THEME-accent);
 			}
 
 			> .after {
 				right: -68px;
-				color: var(--fg);
+				color: var(--MI_THEME-fg);
 			}
 		}
 
@@ -350,11 +350,11 @@ definePageMetadata(() => ({
 				background-color: #749DD6;
 
 				> .before {
-					color: var(--fg);
+					color: var(--MI_THEME-fg);
 				}
 
 				> .after {
-					color: var(--accent);
+					color: var(--MI_THEME-accent);
 				}
 
 				.toggle__handler {
@@ -405,7 +405,7 @@ definePageMetadata(() => ({
 
 	> .sync {
 		padding: 14px 16px;
-		border-top: solid 0.5px var(--divider);
+		border-top: solid 0.5px var(--MI_THEME-divider);
 	}
 }
 
diff --git a/packages/frontend/src/pages/settings/webhook.edit.vue b/packages/frontend/src/pages/settings/webhook.edit.vue
index adeaf8550c..40d23e36c5 100644
--- a/packages/frontend/src/pages/settings/webhook.edit.vue
+++ b/packages/frontend/src/pages/settings/webhook.edit.vue
@@ -184,6 +184,6 @@ definePageMetadata(() => ({
 .description {
 	font-size: 0.85em;
 	padding: 8px 0 0 0;
-	color: var(--fgTransparentWeak);
+	color: var(--MI_THEME-fgTransparentWeak);
 }
 </style>
diff --git a/packages/frontend/src/pages/settings/webhook.vue b/packages/frontend/src/pages/settings/webhook.vue
index 0d11b00c97..af8b7ca945 100644
--- a/packages/frontend/src/pages/settings/webhook.vue
+++ b/packages/frontend/src/pages/settings/webhook.vue
@@ -17,8 +17,8 @@ SPDX-License-Identifier: AGPL-3.0-only
 						<template #icon>
 							<i v-if="webhook.active === false" class="ti ti-player-pause"></i>
 							<i v-else-if="webhook.latestStatus === null" class="ti ti-circle"></i>
-							<i v-else-if="[200, 201, 204].includes(webhook.latestStatus)" class="ti ti-check" :style="{ color: 'var(--success)' }"></i>
-							<i v-else class="ti ti-alert-triangle" :style="{ color: 'var(--error)' }"></i>
+							<i v-else-if="[200, 201, 204].includes(webhook.latestStatus)" class="ti ti-check" :style="{ color: 'var(--MI_THEME-success)' }"></i>
+							<i v-else class="ti ti-alert-triangle" :style="{ color: 'var(--MI_THEME-error)' }"></i>
 						</template>
 						{{ webhook.name || webhook.url }}
 						<template #suffix>
diff --git a/packages/frontend/src/pages/signup-complete.vue b/packages/frontend/src/pages/signup-complete.vue
index 8c2f7042cd..ab8502c1e6 100644
--- a/packages/frontend/src/pages/signup-complete.vue
+++ b/packages/frontend/src/pages/signup-complete.vue
@@ -81,7 +81,7 @@ place-content: center;
 	padding: 16px;
 	text-align: center;
 	font-size: 26px;
-	background-color: var(--accentedBg);
-	color: var(--accent);
+	background-color: var(--MI_THEME-accentedBg);
+	color: var(--MI_THEME-accent);
 }
 </style>
diff --git a/packages/frontend/src/pages/tag.vue b/packages/frontend/src/pages/tag.vue
index 1b3e1ecaee..3e6d4db03d 100644
--- a/packages/frontend/src/pages/tag.vue
+++ b/packages/frontend/src/pages/tag.vue
@@ -78,8 +78,8 @@ definePageMetadata(() => ({
 .footer {
 	-webkit-backdrop-filter: var(--blur, blur(15px));
 	backdrop-filter: var(--blur, blur(15px));
-	background: var(--acrylicBg);
-	border-top: solid 0.5px var(--divider);
+	background: var(--MI_THEME-acrylicBg);
+	border-top: solid 0.5px var(--MI_THEME-divider);
 	display: flex;
 }
 
diff --git a/packages/frontend/src/pages/theme-editor.vue b/packages/frontend/src/pages/theme-editor.vue
index a62fe5d581..b2e084f53f 100644
--- a/packages/frontend/src/pages/theme-editor.vue
+++ b/packages/frontend/src/pages/theme-editor.vue
@@ -268,7 +268,7 @@ definePageMetadata(() => ({
 				}
 
 				&.active {
-					box-shadow: 0 0 0 2px var(--divider) inset;
+					box-shadow: 0 0 0 2px var(--MI_THEME-divider) inset;
 				}
 
 				&.rounded {
diff --git a/packages/frontend/src/pages/timeline.vue b/packages/frontend/src/pages/timeline.vue
index 12e2db2293..f913060096 100644
--- a/packages/frontend/src/pages/timeline.vue
+++ b/packages/frontend/src/pages/timeline.vue
@@ -367,7 +367,7 @@ definePageMetadata(() => ({
 }
 
 .tl {
-	background: var(--bg);
+	background: var(--MI_THEME-bg);
 	border-radius: var(--radius);
 	overflow: clip;
 }
diff --git a/packages/frontend/src/pages/user-list-timeline.vue b/packages/frontend/src/pages/user-list-timeline.vue
index 31a3f1b060..a05743a5a1 100644
--- a/packages/frontend/src/pages/user-list-timeline.vue
+++ b/packages/frontend/src/pages/user-list-timeline.vue
@@ -97,7 +97,7 @@ definePageMetadata(() => ({
 }
 
 .tl {
-	background: var(--bg);
+	background: var(--MI_THEME-bg);
 	border-radius: var(--radius);
 	overflow: clip;
 }
diff --git a/packages/frontend/src/pages/user/clips.vue b/packages/frontend/src/pages/user/clips.vue
index ac01cff8cd..38ce78e8d5 100644
--- a/packages/frontend/src/pages/user/clips.vue
+++ b/packages/frontend/src/pages/user/clips.vue
@@ -43,6 +43,6 @@ const pagination = {
 .description {
 	margin-top: 8px;
 	padding-top: 8px;
-	border-top: solid 0.5px var(--divider);
+	border-top: solid 0.5px var(--MI_THEME-divider);
 }
 </style>
diff --git a/packages/frontend/src/pages/user/home.vue b/packages/frontend/src/pages/user/home.vue
index 111df41127..f0f8724c67 100644
--- a/packages/frontend/src/pages/user/home.vue
+++ b/packages/frontend/src/pages/user/home.vue
@@ -23,7 +23,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 							<MkUserName class="name" :user="user" :nowrap="true"/>
 							<div class="bottom">
 								<span class="username"><MkAcct :user="user" :detail="true"/></span>
-								<span v-if="user.isAdmin" :title="i18n.ts.isAdmin" style="color: var(--badge);"><i class="ti ti-shield"></i></span>
+								<span v-if="user.isAdmin" :title="i18n.ts.isAdmin" style="color: var(--MI_THEME-badge);"><i class="ti ti-shield"></i></span>
 								<span v-if="user.isLocked" :title="i18n.ts.isLocked"><i class="ti ti-lock"></i></span>
 								<span v-if="user.isBot" :title="i18n.ts.isBot"><i class="ti ti-robot"></i></span>
 								<button v-if="$i && !isEditingMemo && !memoDraft" class="_button add-note-button" @click="showMemoTextarea">
@@ -42,7 +42,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 						<MkUserName :user="user" :nowrap="false" class="name"/>
 						<div class="bottom">
 							<span class="username"><MkAcct :user="user" :detail="true"/></span>
-							<span v-if="user.isAdmin" :title="i18n.ts.isAdmin" style="color: var(--badge);"><i class="ti ti-shield"></i></span>
+							<span v-if="user.isAdmin" :title="i18n.ts.isAdmin" style="color: var(--MI_THEME-badge);"><i class="ti ti-shield"></i></span>
 							<span v-if="user.isLocked" :title="i18n.ts.isLocked"><i class="ti ti-lock"></i></span>
 							<span v-if="user.isBot" :title="i18n.ts.isBot"><i class="ti ti-robot"></i></span>
 						</div>
@@ -447,7 +447,7 @@ onUnmounted(() => {
 					text-align: center;
 					padding: 50px 8px 16px 8px;
 					font-weight: bold;
-					border-bottom: solid 0.5px var(--divider);
+					border-bottom: solid 0.5px var(--MI_THEME-divider);
 
 					> .bottom {
 						> * {
@@ -474,7 +474,7 @@ onUnmounted(() => {
 
 					> .fukidashi {
 						display: block;
-						--fukidashi-bg: color-mix(in srgb, var(--accent), var(--panel) 85%);
+						--fukidashi-bg: color-mix(in srgb, var(--MI_THEME-accent), var(--MI_THEME-panel) 85%);
 						--fukidashi-radius: 16px;
 						font-size: 0.9em;
 
@@ -493,7 +493,7 @@ onUnmounted(() => {
 					gap: 8px;
 
 					> .role {
-						border: solid 1px var(--color, var(--divider));
+						border: solid 1px var(--color, var(--MI_THEME-divider));
 						border-radius: 999px;
 						margin-right: 4px;
 						padding: 3px 8px;
@@ -507,15 +507,15 @@ onUnmounted(() => {
 				> .memo {
 					margin: 12px 24px 0 154px;
 					background: transparent;
-					color: var(--fg);
-					border: 1px solid var(--divider);
+					color: var(--MI_THEME-fg);
+					border: 1px solid var(--MI_THEME-divider);
 					border-radius: 8px;
 					padding: 8px;
 					line-height: 0;
 
 					> .heading {
 						text-align: left;
-						color: var(--fgTransparent);
+						color: var(--MI_THEME-fgTransparent);
 						line-height: 1.5;
 						font-size: 85%;
 					}
@@ -530,7 +530,7 @@ onUnmounted(() => {
 						height: auto;
 						min-height: 0;
 						line-height: 1.5;
-						color: var(--fg);
+						color: var(--MI_THEME-fg);
 						overflow: hidden;
 						background: transparent;
 						font-family: inherit;
@@ -550,7 +550,7 @@ onUnmounted(() => {
 				> .fields {
 					padding: 24px;
 					font-size: 0.9em;
-					border-top: solid 0.5px var(--divider);
+					border-top: solid 0.5px var(--MI_THEME-divider);
 
 					> .field {
 						display: flex;
@@ -587,14 +587,14 @@ onUnmounted(() => {
 				> .status {
 					display: flex;
 					padding: 24px;
-					border-top: solid 0.5px var(--divider);
+					border-top: solid 0.5px var(--MI_THEME-divider);
 
 					> a {
 						flex: 1;
 						text-align: center;
 
 						&.active {
-							color: var(--accent);
+							color: var(--MI_THEME-accent);
 						}
 
 						&:hover {
@@ -710,13 +710,13 @@ onUnmounted(() => {
 
 <style lang="scss" module>
 .tl {
-	background: var(--bg);
+	background: var(--MI_THEME-bg);
 	border-radius: var(--radius);
 	overflow: clip;
 }
 
 .verifiedLink {
 	margin-left: 4px;
-	color: var(--success);
+	color: var(--MI_THEME-success);
 }
 </style>
diff --git a/packages/frontend/src/pages/user/index.timeline.vue b/packages/frontend/src/pages/user/index.timeline.vue
index 8dbf90f344..6339c54ddf 100644
--- a/packages/frontend/src/pages/user/index.timeline.vue
+++ b/packages/frontend/src/pages/user/index.timeline.vue
@@ -52,11 +52,11 @@ const pagination = computed(() => tab.value === 'featured' ? {
 <style lang="scss" module>
 .tab {
 	padding: calc(var(--margin) / 2) 0;
-	background: var(--bg);
+	background: var(--MI_THEME-bg);
 }
 
 .tl {
-	background: var(--bg);
+	background: var(--MI_THEME-bg);
 	border-radius: var(--radius);
 	overflow: clip;
 }
diff --git a/packages/frontend/src/pages/user/lists.vue b/packages/frontend/src/pages/user/lists.vue
index 5e9b95eb74..00de3e9132 100644
--- a/packages/frontend/src/pages/user/lists.vue
+++ b/packages/frontend/src/pages/user/lists.vue
@@ -44,12 +44,12 @@ const pagination = {
 .list {
 	display: block;
 	padding: 16px;
-	border: solid 1px var(--divider);
+	border: solid 1px var(--MI_THEME-divider);
 	border-radius: 6px;
 	margin-bottom: 8px;
 
 	&:hover {
-		border: solid 1px var(--accent);
+		border: solid 1px var(--MI_THEME-accent);
 		text-decoration: none;
 	}
 }
diff --git a/packages/frontend/src/pages/user/raw.vue b/packages/frontend/src/pages/user/raw.vue
index dd57048409..e6e66bd6af 100644
--- a/packages/frontend/src/pages/user/raw.vue
+++ b/packages/frontend/src/pages/user/raw.vue
@@ -113,18 +113,18 @@ const suspended = computed(() => props.user.isSuspended ?? false);
 	}
 
 	> .suspended {
-		color: var(--error);
-		border-color: var(--error);
+		color: var(--MI_THEME-error);
+		border-color: var(--MI_THEME-error);
 	}
 
 	> .silenced {
-		color: var(--warn);
-		border-color: var(--warn);
+		color: var(--MI_THEME-warn);
+		border-color: var(--MI_THEME-warn);
 	}
 
 	> .moderator {
-		color: var(--success);
-		border-color: var(--success);
+		color: var(--MI_THEME-success);
+		border-color: var(--MI_THEME-success);
 	}
 }
 </style>
diff --git a/packages/frontend/src/pages/user/reactions.vue b/packages/frontend/src/pages/user/reactions.vue
index 3671decc18..7168778e12 100644
--- a/packages/frontend/src/pages/user/reactions.vue
+++ b/packages/frontend/src/pages/user/reactions.vue
@@ -44,7 +44,7 @@ const pagination = {
 	align-items: center;
 	padding: 8px 16px;
 	margin-bottom: 8px;
-	border-bottom: solid 2px var(--divider);
+	border-bottom: solid 2px var(--MI_THEME-divider);
 }
 
 .avatar {
diff --git a/packages/frontend/src/pages/welcome.entrance.a.vue b/packages/frontend/src/pages/welcome.entrance.a.vue
index d6ba397f1b..8e1f9a4a2c 100644
--- a/packages/frontend/src/pages/welcome.entrance.a.vue
+++ b/packages/frontend/src/pages/welcome.entrance.a.vue
@@ -98,7 +98,7 @@ misskeyApiGet('federation/instances', {
 		left: 0;
 		width: 100vw;
 		height: 100vh;
-		background: var(--accent);
+		background: var(--MI_THEME-accent);
 		clip-path: polygon(0% 0%, 45% 0%, 20% 100%, 0% 100%);
 	}
 	> .shape2 {
@@ -107,7 +107,7 @@ misskeyApiGet('federation/instances', {
 		left: 0;
 		width: 100vw;
 		height: 100vh;
-		background: var(--accent);
+		background: var(--MI_THEME-accent);
 		clip-path: polygon(0% 0%, 25% 0%, 35% 100%, 0% 100%);
 		opacity: 0.5;
 	}
@@ -164,7 +164,7 @@ misskeyApiGet('federation/instances', {
 		left: 0;
 		right: 0;
 		margin: auto;
-		background: var(--acrylicPanel);
+		background: var(--MI_THEME-acrylicPanel);
 		-webkit-backdrop-filter: var(--blur, blur(15px));
 		backdrop-filter: var(--blur, blur(15px));
 		border-radius: 999px;
@@ -186,7 +186,7 @@ misskeyApiGet('federation/instances', {
 	vertical-align: bottom;
 	padding: 6px 12px 6px 6px;
 	margin: 0 10px 0 0;
-	background: var(--panel);
+	background: var(--MI_THEME-panel);
 	border-radius: 999px;
 
 	> :global(.icon) {
diff --git a/packages/frontend/src/pages/welcome.setup.vue b/packages/frontend/src/pages/welcome.setup.vue
index dd258aad98..6174bcd820 100644
--- a/packages/frontend/src/pages/welcome.setup.vue
+++ b/packages/frontend/src/pages/welcome.setup.vue
@@ -110,8 +110,8 @@ function submit() {
 	font-size: 1.5em;
 	text-align: center;
 	padding: 32px;
-	background: var(--accentedBg);
-	color: var(--accent);
+	background: var(--MI_THEME-accentedBg);
+	color: var(--MI_THEME-accent);
 	font-weight: bold;
 }
 
diff --git a/packages/frontend/src/pages/welcome.timeline.note.vue b/packages/frontend/src/pages/welcome.timeline.note.vue
index 6a9ecd9a62..8fb84fd58f 100644
--- a/packages/frontend/src/pages/welcome.timeline.note.vue
+++ b/packages/frontend/src/pages/welcome.timeline.note.vue
@@ -84,7 +84,7 @@ onUpdated(() => {
 		left: 0;
 		width: 100%;
 		height: 64px;
-		background: linear-gradient(0deg, var(--panel), color(from var(--panel) srgb r g b / 0));
+		background: linear-gradient(0deg, var(--MI_THEME-panel), color(from var(--MI_THEME-panel) srgb r g b / 0));
 	}
 }
 
@@ -100,7 +100,7 @@ onUpdated(() => {
 	margin: 8px -16px -8px;
 	padding: 8px 16px 0;
 	width: calc(100% + 32px);
-	border-top: 1px solid var(--divider);
+	border-top: 1px solid var(--MI_THEME-divider);
 }
 
 .richcontent {
diff --git a/packages/frontend/src/scripts/init-chart.ts b/packages/frontend/src/scripts/init-chart.ts
index 2465a14703..41e1636aa7 100644
--- a/packages/frontend/src/scripts/init-chart.ts
+++ b/packages/frontend/src/scripts/init-chart.ts
@@ -50,7 +50,7 @@ export function initChart() {
 	);
 
 	// フォントカラー
-	Chart.defaults.color = getComputedStyle(document.documentElement).getPropertyValue('--fg');
+	Chart.defaults.color = getComputedStyle(document.documentElement).getPropertyValue('--MI_THEME-fg');
 
 	Chart.defaults.borderColor = defaultStore.state.darkMode ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)';
 
diff --git a/packages/frontend/src/scripts/theme.ts b/packages/frontend/src/scripts/theme.ts
index b7e7a5a3f8..1a3909c132 100644
--- a/packages/frontend/src/scripts/theme.ts
+++ b/packages/frontend/src/scripts/theme.ts
@@ -94,7 +94,7 @@ export function applyTheme(theme: Theme, persist = true) {
 	}
 
 	for (const [k, v] of Object.entries(props)) {
-		document.documentElement.style.setProperty(`--${k}`, v.toString());
+		document.documentElement.style.setProperty(`--MI_THEME-${k}`, v.toString());
 	}
 
 	document.documentElement.style.setProperty('color-scheme', colorScheme);
diff --git a/packages/frontend/src/style.scss b/packages/frontend/src/style.scss
index b835096b15..424cc02d0e 100644
--- a/packages/frontend/src/style.scss
+++ b/packages/frontend/src/style.scss
@@ -25,14 +25,14 @@
 }
 
 ::selection {
-	color: var(--fgOnAccent);
-	background-color: var(--accent);
+	color: var(--MI_THEME-fgOnAccent);
+	background-color: var(--MI_THEME-accent);
 }
 
 html {
-	background-color: var(--bg);
-	color: var(--fg);
-	accent-color: var(--accent);
+	background-color: var(--MI_THEME-bg);
+	color: var(--MI_THEME-fg);
+	accent-color: var(--MI_THEME-accent);
 	overflow: auto;
 	overflow-wrap: break-word;
 	font-family: 'Hiragino Maru Gothic Pro', "BIZ UDGothic", Roboto, HelveticaNeue, Arial, sans-serif;
@@ -43,7 +43,7 @@ html {
 	-webkit-text-size-adjust: 100%;
 
 	&, * {
-		scrollbar-color: var(--scrollbarHandle) transparent;
+		scrollbar-color: var(--MI_THEME-scrollbarHandle) transparent;
 		scrollbar-width: thin;
 
 		&::-webkit-scrollbar {
@@ -56,14 +56,14 @@ html {
 		}
 
 		&::-webkit-scrollbar-thumb {
-			background: var(--scrollbarHandle);
+			background: var(--MI_THEME-scrollbarHandle);
 
 			&:hover {
-				background: var(--scrollbarHandleHover);
+				background: var(--MI_THEME-scrollbarHandleHover);
 			}
 
 			&:active {
-				background: var(--accent);
+				background: var(--MI_THEME-accent);
 			}
 		}
 	}
@@ -125,15 +125,15 @@ textarea, input {
 }
 
 optgroup, option {
-	background: var(--panel);
-	color: var(--fg);
+	background: var(--MI_THEME-panel);
+	color: var(--MI_THEME-fg);
 }
 
 hr {
 	margin: var(--margin) 0 var(--margin) 0;
 	border: none;
 	height: 1px;
-	background: var(--divider);
+	background: var(--MI_THEME-divider);
 }
 
 rt {
@@ -141,7 +141,7 @@ rt {
 }
 
 :focus-visible {
-	outline: var(--focus) solid 2px;
+	outline: var(--MI_THEME-focus) solid 2px;
 	outline-offset: -2px;
 
 	&:hover {
@@ -174,9 +174,9 @@ rt {
 
 ._indicateCounter {
 	display: inline-flex;
-	color: var(--fgOnAccent);
+	color: var(--MI_THEME-fgOnAccent);
 	font-weight: 700;
-	background: var(--indicator);
+	background: var(--MI_THEME-indicator);
 	height: 1.5em;
 	min-width: 1.5em;
 	align-items: center;
@@ -209,13 +209,13 @@ rt {
 	left: 0;
 	width: 100%;
 	height: 100%;
-	background: var(--modalBg);
+	background: var(--MI_THEME-modalBg);
 	-webkit-backdrop-filter: var(--modalBgFilter);
 	backdrop-filter: var(--modalBgFilter);
 }
 
 ._shadow {
-	box-shadow: 0px 4px 32px var(--shadow) !important;
+	box-shadow: 0px 4px 32px var(--MI_THEME-shadow) !important;
 }
 
 ._button {
@@ -244,40 +244,40 @@ rt {
 
 ._buttonPrimary {
 	@extend ._button;
-	color: var(--fgOnAccent);
-	background: var(--accent);
+	color: var(--MI_THEME-fgOnAccent);
+	background: var(--MI_THEME-accent);
 
 	&:not(:disabled):hover {
-		background: hsl(from var(--accent) h s calc(l + 5));
+		background: hsl(from var(--MI_THEME-accent) h s calc(l + 5));
 	}
 
 	&:not(:disabled):active {
-		background: hsl(from var(--accent) h s calc(l - 5));
+		background: hsl(from var(--MI_THEME-accent) h s calc(l - 5));
 	}
 }
 
 ._buttonGradate {
 	@extend ._buttonPrimary;
-	color: var(--fgOnAccent);
-	background: linear-gradient(90deg, var(--buttonGradateA), var(--buttonGradateB));
+	color: var(--MI_THEME-fgOnAccent);
+	background: linear-gradient(90deg, var(--MI_THEME-buttonGradateA), var(--MI_THEME-buttonGradateB));
 
 	&:not(:disabled):hover {
-		background: linear-gradient(90deg, hsl(from var(--accent) h s calc(l + 5)), hsl(from var(--accent) h s calc(l + 5)));
+		background: linear-gradient(90deg, hsl(from var(--MI_THEME-accent) h s calc(l + 5)), hsl(from var(--MI_THEME-accent) h s calc(l + 5)));
 	}
 
 	&:not(:disabled):active {
-		background: linear-gradient(90deg, hsl(from var(--accent) h s calc(l + 5)), hsl(from var(--accent) h s calc(l + 5)));
+		background: linear-gradient(90deg, hsl(from var(--MI_THEME-accent) h s calc(l + 5)), hsl(from var(--MI_THEME-accent) h s calc(l + 5)));
 	}
 }
 
 ._help {
-	color: var(--accent);
+	color: var(--MI_THEME-accent);
 	cursor: help;
 }
 
 ._textButton {
 	@extend ._button;
-	color: var(--accent);
+	color: var(--MI_THEME-accent);
 
 	&:focus-visible {
 		outline-offset: 2px;
@@ -289,7 +289,7 @@ rt {
 }
 
 ._panel {
-	background: var(--panel);
+	background: var(--MI_THEME-panel);
 	border-radius: var(--radius);
 	overflow: clip;
 }
@@ -335,22 +335,22 @@ rt {
 	padding: 10px;
 	box-sizing: border-box;
 	text-align: center;
-	border: solid 0.5px var(--divider);
+	border: solid 0.5px var(--MI_THEME-divider);
 	border-radius: var(--radius);
 
 	&:active {
-		border-color: var(--accent);
+		border-color: var(--MI_THEME-accent);
 	}
 }
 
 ._popup {
-	background: var(--popup);
+	background: var(--MI_THEME-popup);
 	border-radius: var(--radius);
 	contain: content;
 }
 
 ._acrylic {
-	background: var(--acrylicPanel);
+	background: var(--MI_THEME-acrylicPanel);
 	-webkit-backdrop-filter: var(--blur, blur(15px));
 	backdrop-filter: var(--blur, blur(15px));
 }
@@ -365,8 +365,8 @@ rt {
 	margin-left: 0.7em;
 	font-size: 65%;
 	padding: 2px 3px;
-	color: var(--accent);
-	border: solid 1px var(--accent);
+	color: var(--MI_THEME-accent);
+	border: solid 1px var(--MI_THEME-accent);
 	border-radius: 4px;
 	vertical-align: top;
 }
@@ -375,8 +375,8 @@ rt {
 	margin-left: 0.7em;
 	font-size: 65%;
 	padding: 2px 3px;
-	color: var(--warn);
-	border: solid 1px var(--warn);
+	color: var(--MI_THEME-warn);
+	border: solid 1px var(--MI_THEME-warn);
 	border-radius: 4px;
 	vertical-align: top;
 }
@@ -422,7 +422,7 @@ rt {
 }
 
 ._link {
-	color: var(--link);
+	color: var(--MI_THEME-link);
 }
 
 ._caption {
@@ -446,14 +446,14 @@ rt {
 	box-shadow: 0 6px 16px #0007, 0 0 1px 1px #693410, inset 0 0 2px 1px #ce8a5c;
 	border-radius: 10px;
 
-	--bg: #F1E8DC;
-	--fg: #693410;
+	--MI_THEME-bg: #F1E8DC;
+	--MI_THEME-fg: #693410;
 }
 
 html[data-color-scheme=dark] ._woodenFrame {
-	--bg: #1d0c02;
-	--fg: #F1E8DC;
-	--panel: #192320;
+	--MI_THEME-bg: #1d0c02;
+	--MI_THEME-fg: #F1E8DC;
+	--MI_THEME-panel: #192320;
 }
 
 ._woodenFrameH {
@@ -464,10 +464,10 @@ html[data-color-scheme=dark] ._woodenFrame {
 ._woodenFrameInner {
 	padding: 8px;
 	margin-top: 8px;
-	background: var(--bg);
+	background: var(--MI_THEME-bg);
 	box-shadow: 0 0 2px 1px #ce8a5c, inset 0 0 1px 1px #693410;
 	border-radius: 6px;
-	color: var(--fg);
+	color: var(--MI_THEME-fg);
 
 	&:first-child {
 		margin-top: 0;
diff --git a/packages/frontend/src/ui/_common_/announcements.vue b/packages/frontend/src/ui/_common_/announcements.vue
index 374bc20b54..d153dc8726 100644
--- a/packages/frontend/src/ui/_common_/announcements.vue
+++ b/packages/frontend/src/ui/_common_/announcements.vue
@@ -13,9 +13,9 @@ SPDX-License-Identifier: AGPL-3.0-only
 	>
 		<span :class="$style.icon">
 			<i v-if="announcement.icon === 'info'" class="ti ti-info-circle"></i>
-			<i v-else-if="announcement.icon === 'warning'" class="ti ti-alert-triangle" style="color: var(--warn);"></i>
-			<i v-else-if="announcement.icon === 'error'" class="ti ti-circle-x" style="color: var(--error);"></i>
-			<i v-else-if="announcement.icon === 'success'" class="ti ti-check" style="color: var(--success);"></i>
+			<i v-else-if="announcement.icon === 'warning'" class="ti ti-alert-triangle" style="color: var(--MI_THEME-warn);"></i>
+			<i v-else-if="announcement.icon === 'error'" class="ti ti-circle-x" style="color: var(--MI_THEME-error);"></i>
+			<i v-else-if="announcement.icon === 'success'" class="ti ti-check" style="color: var(--MI_THEME-success);"></i>
 		</span>
 		<span :class="$style.title">{{ announcement.title }}</span>
 		<span :class="$style.body">{{ announcement.text }}</span>
@@ -30,7 +30,7 @@ import { $i } from '@/account.js';
 <style lang="scss" module>
 .root {
 	font-size: 15px;
-	background: var(--panel);
+	background: var(--MI_THEME-panel);
 }
 
 .item {
@@ -44,8 +44,8 @@ import { $i } from '@/account.js';
 	height: var(--height);
 	overflow: clip;
 	contain: strict;
-	background: var(--accent);
-	color: var(--fgOnAccent);
+	background: var(--MI_THEME-accent);
+	color: var(--MI_THEME-fgOnAccent);
 
 	@container (max-width: 1000px) {
 		display: block;
diff --git a/packages/frontend/src/ui/_common_/common.vue b/packages/frontend/src/ui/_common_/common.vue
index 61881c02e1..e3c0f1f4ce 100644
--- a/packages/frontend/src/ui/_common_/common.vue
+++ b/packages/frontend/src/ui/_common_/common.vue
@@ -234,8 +234,8 @@ if ($i) {
 		height: 18px;
 		box-sizing: border-box;
 		border: solid 2px transparent;
-		border-top-color: var(--accent);
-		border-left-color: var(--accent);
+		border-top-color: var(--MI_THEME-accent);
+		border-left-color: var(--MI_THEME-accent);
 		border-radius: 50%;
 		animation: progress-spinner 400ms linear infinite;
 	}
diff --git a/packages/frontend/src/ui/_common_/navbar-for-mobile.vue b/packages/frontend/src/ui/_common_/navbar-for-mobile.vue
index 5115d21d56..a71f57670d 100644
--- a/packages/frontend/src/ui/_common_/navbar-for-mobile.vue
+++ b/packages/frontend/src/ui/_common_/navbar-for-mobile.vue
@@ -82,7 +82,7 @@ function more() {
 
 <style lang="scss" module>
 .root {
-	--nav-bg-transparent: color(from var(--navBg) srgb r g b / 0.5);
+	--nav-bg-transparent: color(from var(--MI_THEME-navBg) srgb r g b / 0.5);
 
 	display: flex;
 	flex-direction: column;
@@ -137,7 +137,7 @@ function more() {
 	display: block;
 	width: 100%;
 	height: 40px;
-	color: var(--fgOnAccent);
+	color: var(--MI_THEME-fgOnAccent);
 	font-weight: bold;
 	text-align: left;
 
@@ -153,12 +153,12 @@ function more() {
 		right: 0;
 		bottom: 0;
 		border-radius: 999px;
-		background: linear-gradient(90deg, var(--buttonGradateA), var(--buttonGradateB));
+		background: linear-gradient(90deg, var(--MI_THEME-buttonGradateA), var(--MI_THEME-buttonGradateB));
 	}
 
 	&:hover, &.active {
 		&::before {
-			background: var(--accentLighten);
+			background: var(--MI_THEME-accentLighten);
 		}
 	}
 }
@@ -202,7 +202,7 @@ function more() {
 
 .divider {
 	margin: 16px 16px;
-	border-top: solid 0.5px var(--divider);
+	border-top: solid 0.5px var(--MI_THEME-divider);
 }
 
 .item {
@@ -216,15 +216,15 @@ function more() {
 	width: 100%;
 	text-align: left;
 	box-sizing: border-box;
-	color: var(--navFg);
+	color: var(--MI_THEME-navFg);
 
 	&:hover {
 		text-decoration: none;
-		color: var(--navHoverFg);
+		color: var(--MI_THEME-navHoverFg);
 	}
 
 	&.active {
-		color: var(--navActive);
+		color: var(--MI_THEME-navActive);
 	}
 
 	&:hover, &.active {
@@ -240,7 +240,7 @@ function more() {
 			right: 0;
 			bottom: 0;
 			border-radius: 999px;
-			background: var(--accentedBg);
+			background: var(--MI_THEME-accentedBg);
 		}
 	}
 }
@@ -255,7 +255,7 @@ function more() {
 	position: absolute;
 	top: 0;
 	left: 20px;
-	color: var(--navIndicator);
+	color: var(--MI_THEME-navIndicator);
 	font-size: 8px;
 	animation: global-blink 1s infinite;
 
diff --git a/packages/frontend/src/ui/_common_/navbar.vue b/packages/frontend/src/ui/_common_/navbar.vue
index 05f5ff2565..4d01330432 100644
--- a/packages/frontend/src/ui/_common_/navbar.vue
+++ b/packages/frontend/src/ui/_common_/navbar.vue
@@ -111,7 +111,7 @@ function more(ev: MouseEvent) {
 .root {
 	--nav-width: 250px;
 	--nav-icon-only-width: 80px;
-	--nav-bg-transparent: color(from var(--navBg) srgb r g b / 0.5);
+	--nav-bg-transparent: color(from var(--MI_THEME-navBg) srgb r g b / 0.5);
 
 	flex: 0 0 var(--nav-width);
 	width: var(--nav-width);
@@ -129,7 +129,7 @@ function more(ev: MouseEvent) {
 	overflow: auto;
 	overflow-x: clip;
 	overscroll-behavior: contain;
-	background: var(--navBg);
+	background: var(--MI_THEME-navBg);
 	contain: strict;
 	display: flex;
 	flex-direction: column;
@@ -172,7 +172,7 @@ function more(ev: MouseEvent) {
 			outline: none;
 
 			> .instanceIcon {
-				outline: 2px solid var(--focus);
+				outline: 2px solid var(--MI_THEME-focus);
 				outline-offset: 2px;
 			}
 		}
@@ -198,7 +198,7 @@ function more(ev: MouseEvent) {
 		display: block;
 		width: 100%;
 		height: 40px;
-		color: var(--fgOnAccent);
+		color: var(--MI_THEME-fgOnAccent);
 		font-weight: bold;
 		text-align: left;
 
@@ -214,21 +214,21 @@ function more(ev: MouseEvent) {
 			right: 0;
 			bottom: 0;
 			border-radius: 999px;
-			background: linear-gradient(90deg, var(--buttonGradateA), var(--buttonGradateB));
+			background: linear-gradient(90deg, var(--MI_THEME-buttonGradateA), var(--MI_THEME-buttonGradateB));
 		}
 
 		&:focus-visible {
 			outline: none;
 
 			&::before {
-				outline: 2px solid var(--fgOnAccent);
+				outline: 2px solid var(--MI_THEME-fgOnAccent);
 				outline-offset: -4px;
 			}
 		}
 
 		&:hover, &.active {
 			&::before {
-				background: var(--accentLighten);
+				background: var(--MI_THEME-accentLighten);
 			}
 		}
 	}
@@ -258,7 +258,7 @@ function more(ev: MouseEvent) {
 			outline: none;
 
 			> .avatar {
-				box-shadow: 0 0 0 4px var(--focus);
+				box-shadow: 0 0 0 4px var(--MI_THEME-focus);
 			}
 		}
 	}
@@ -284,7 +284,7 @@ function more(ev: MouseEvent) {
 
 	.divider {
 		margin: 16px 16px;
-		border-top: solid 0.5px var(--divider);
+		border-top: solid 0.5px var(--MI_THEME-divider);
 	}
 
 	.item {
@@ -298,28 +298,28 @@ function more(ev: MouseEvent) {
 		width: 100%;
 		text-align: left;
 		box-sizing: border-box;
-		color: var(--navFg);
+		color: var(--MI_THEME-navFg);
 
 		&:hover {
 			text-decoration: none;
-			color: var(--navHoverFg);
+			color: var(--MI_THEME-navHoverFg);
 		}
 
 		&.active {
-			color: var(--navActive);
+			color: var(--MI_THEME-navActive);
 		}
 
 		&:focus-visible {
 			outline: none;
 
 			&::before {
-				outline: 2px solid var(--focus);
+				outline: 2px solid var(--MI_THEME-focus);
 				outline-offset: -2px;
 			}
 		}
 
 		&:hover, &.active, &:focus {
-			color: var(--accent);
+			color: var(--MI_THEME-accent);
 
 			&::before {
 				content: "";
@@ -333,7 +333,7 @@ function more(ev: MouseEvent) {
 				right: 0;
 				bottom: 0;
 				border-radius: 999px;
-				background: var(--accentedBg);
+				background: var(--MI_THEME-accentedBg);
 			}
 		}
 	}
@@ -348,7 +348,7 @@ function more(ev: MouseEvent) {
 		position: absolute;
 		top: 0;
 		left: 20px;
-		color: var(--navIndicator);
+		color: var(--MI_THEME-navIndicator);
 		font-size: 8px;
 		animation: global-blink 1s infinite;
 
@@ -393,7 +393,7 @@ function more(ev: MouseEvent) {
 			outline: none;
 
 			> .instanceIcon {
-				outline: 2px solid var(--focus);
+				outline: 2px solid var(--MI_THEME-focus);
 				outline-offset: 2px;
 			}
 		}
@@ -433,28 +433,28 @@ function more(ev: MouseEvent) {
 			width: 52px;
 			aspect-ratio: 1/1;
 			border-radius: 100%;
-			background: linear-gradient(90deg, var(--buttonGradateA), var(--buttonGradateB));
+			background: linear-gradient(90deg, var(--MI_THEME-buttonGradateA), var(--MI_THEME-buttonGradateB));
 		}
 
 		&:focus-visible {
 			outline: none;
 
 			&::before {
-				outline: 2px solid var(--fgOnAccent);
+				outline: 2px solid var(--MI_THEME-fgOnAccent);
 				outline-offset: -4px;
 			}
 		}
 
 		&:hover, &.active {
 			&::before {
-				background: var(--accentLighten);
+				background: var(--MI_THEME-accentLighten);
 			}
 		}
 	}
 
 	.postIcon {
 		position: relative;
-		color: var(--fgOnAccent);
+		color: var(--MI_THEME-fgOnAccent);
 	}
 
 	.postText {
@@ -472,7 +472,7 @@ function more(ev: MouseEvent) {
 			outline: none;
 
 			> .avatar {
-				box-shadow: 0 0 0 4px var(--focus);
+				box-shadow: 0 0 0 4px var(--MI_THEME-focus);
 			}
 		}
 	}
@@ -494,7 +494,7 @@ function more(ev: MouseEvent) {
 	.divider {
 		margin: 8px auto;
 		width: calc(100% - 32px);
-		border-top: solid 0.5px var(--divider);
+		border-top: solid 0.5px var(--MI_THEME-divider);
 	}
 
 	.item {
@@ -508,14 +508,14 @@ function more(ev: MouseEvent) {
 			outline: none;
 
 			&::before {
-				outline: 2px solid var(--focus);
+				outline: 2px solid var(--MI_THEME-focus);
 				outline-offset: -2px;
 			}
 		}
 
 		&:hover, &.active, &:focus {
 			text-decoration: none;
-			color: var(--accent);
+			color: var(--MI_THEME-accent);
 
 			&::before {
 				content: "";
@@ -529,7 +529,7 @@ function more(ev: MouseEvent) {
 				right: 0;
 				bottom: 0;
 				border-radius: 999px;
-				background: var(--accentedBg);
+				background: var(--MI_THEME-accentedBg);
 			}
 
 			> .icon,
@@ -553,7 +553,7 @@ function more(ev: MouseEvent) {
 		position: absolute;
 		top: 6px;
 		left: 24px;
-		color: var(--navIndicator);
+		color: var(--MI_THEME-navIndicator);
 		font-size: 8px;
 		animation: global-blink 1s infinite;
 
diff --git a/packages/frontend/src/ui/_common_/statusbars.vue b/packages/frontend/src/ui/_common_/statusbars.vue
index 690366307b..5f9a938017 100644
--- a/packages/frontend/src/ui/_common_/statusbars.vue
+++ b/packages/frontend/src/ui/_common_/statusbars.vue
@@ -32,7 +32,7 @@ const XUserList = defineAsyncComponent(() => import('./statusbar-user-list.vue')
 <style lang="scss" module>
 .root {
 	font-size: 15px;
-	background: var(--panel);
+	background: var(--MI_THEME-panel);
 }
 
 .item {
@@ -81,7 +81,7 @@ const XUserList = defineAsyncComponent(() => import('./statusbar-user-list.vue')
 .name {
 	padding: 0 var(--nameMargin);
 	font-weight: bold;
-	color: var(--accent);
+	color: var(--MI_THEME-accent);
 
 	&:empty {
 		display: none;
diff --git a/packages/frontend/src/ui/_common_/upload.vue b/packages/frontend/src/ui/_common_/upload.vue
index 6db7f9cae7..c7d1387eae 100644
--- a/packages/frontend/src/ui/_common_/upload.vue
+++ b/packages/frontend/src/ui/_common_/upload.vue
@@ -125,10 +125,10 @@ const zIndex = os.claimZIndex('high');
 	height: 8px;
 }
 .mk-uploader > ol > li > progress::-webkit-progress-value {
-  background: var(--accent);
+  background: var(--MI_THEME-accent);
 }
 .mk-uploader > ol > li > progress::-webkit-progress-bar {
-  //background: var(--accentAlpha01);
+  //background: var(--MI_THEME-accentAlpha01);
 	background: transparent;
 }
 </style>
diff --git a/packages/frontend/src/ui/classic.header.vue b/packages/frontend/src/ui/classic.header.vue
index c03afd6cd6..a0a8601887 100644
--- a/packages/frontend/src/ui/classic.header.vue
+++ b/packages/frontend/src/ui/classic.header.vue
@@ -104,7 +104,7 @@ onMounted(() => {
 	z-index: 1000;
 	width: 100%;
 	height: $height;
-	background-color: var(--bg);
+	background-color: var(--MI_THEME-bg);
 
 	> .body {
 		max-width: 1380px;
@@ -140,18 +140,18 @@ onMounted(() => {
 					position: absolute;
 					top: 0;
 					left: 0;
-					color: var(--navIndicator);
+					color: var(--MI_THEME-navIndicator);
 					font-size: 8px;
 					animation: global-blink 1s infinite;
 				}
 
 				&:hover {
 					text-decoration: none;
-					color: var(--navHoverFg);
+					color: var(--MI_THEME-navHoverFg);
 				}
 
 				&.active {
-					color: var(--navActive);
+					color: var(--MI_THEME-navActive);
 				}
 			}
 
@@ -159,7 +159,7 @@ onMounted(() => {
 				display: inline-block;
 				height: 16px;
 				margin: 0 10px;
-				border-right: solid 0.5px var(--divider);
+				border-right: solid 0.5px var(--MI_THEME-divider);
 			}
 
 			> .instance {
diff --git a/packages/frontend/src/ui/classic.sidebar.vue b/packages/frontend/src/ui/classic.sidebar.vue
index 87b4515d46..4d1846c34c 100644
--- a/packages/frontend/src/ui/classic.sidebar.vue
+++ b/packages/frontend/src/ui/classic.sidebar.vue
@@ -157,7 +157,7 @@ watch(defaultStore.reactiveState.menuDisplay, () => {
 
 	> .divider {
 		margin: 10px 0;
-		border-top: solid 0.5px var(--divider);
+		border-top: solid 0.5px var(--MI_THEME-divider);
 	}
 
 	> .post {
@@ -165,7 +165,7 @@ watch(defaultStore.reactiveState.menuDisplay, () => {
 		top: 0;
 		z-index: 1;
 		padding: 16px 0;
-		background: var(--bg);
+		background: var(--MI_THEME-bg);
 
 		> .button {
 			min-width: 0;
@@ -220,7 +220,7 @@ watch(defaultStore.reactiveState.menuDisplay, () => {
 			position: absolute;
 			top: 0;
 			left: 0;
-			color: var(--navIndicator);
+			color: var(--MI_THEME-navIndicator);
 			font-size: 8px;
 			animation: global-blink 1s infinite;
 
@@ -233,11 +233,11 @@ watch(defaultStore.reactiveState.menuDisplay, () => {
 
 		&:hover {
 			text-decoration: none;
-			color: var(--navHoverFg);
+			color: var(--MI_THEME-navHoverFg);
 		}
 
 		&.active {
-			color: var(--navActive);
+			color: var(--MI_THEME-navActive);
 		}
 	}
 }
diff --git a/packages/frontend/src/ui/classic.vue b/packages/frontend/src/ui/classic.vue
index fa04409d2d..9715e1ba18 100644
--- a/packages/frontend/src/ui/classic.vue
+++ b/packages/frontend/src/ui/classic.vue
@@ -216,7 +216,7 @@ onMounted(() => {
 	box-sizing: border-box;
 
 	&.wallpaper {
-		background: var(--wallpaperOverlay);
+		background: var(--MI_THEME-wallpaperOverlay);
 		//backdrop-filter: var(--blur, blur(4px));
 	}
 
@@ -249,15 +249,15 @@ onMounted(() => {
 			min-width: 0;
 			width: 750px;
 			margin: 0 16px 0 0;
-			border-left: solid 1px var(--divider);
-			border-right: solid 1px var(--divider);
+			border-left: solid 1px var(--MI_THEME-divider);
+			border-right: solid 1px var(--MI_THEME-divider);
 			border-radius: 0;
 			overflow: clip;
 			--margin: 12px;
 		}
 
 		> .widgets {
-			//--panelBorder: none;
+			//--MI_THEME-panelBorder: none;
 			width: 300px;
 			padding-bottom: calc(var(--margin) + env(safe-area-inset-bottom, 0px));
 
@@ -277,7 +277,7 @@ onMounted(() => {
 		&.withGlobalHeader {
 			> .main {
 				margin-top: 0;
-				border: solid 1px var(--divider);
+				border: solid 1px var(--MI_THEME-divider);
 				border-radius: var(--radius);
 				--stickyTop: var(--globalHeaderHeight);
 			}
@@ -292,7 +292,7 @@ onMounted(() => {
 			margin: 0;
 
 			> .sidebar {
-				border-right: solid 0.5px var(--divider);
+				border-right: solid 0.5px var(--MI_THEME-divider);
 			}
 
 			> .main {
@@ -317,7 +317,7 @@ onMounted(() => {
 		padding: var(--margin) var(--margin) calc(var(--margin) + env(safe-area-inset-bottom, 0px));
 		box-sizing: border-box;
 		overflow: auto;
-		background: var(--bg);
+		background: var(--MI_THEME-bg);
 	}
 
 	> .ivnzpscs {
diff --git a/packages/frontend/src/ui/deck.vue b/packages/frontend/src/ui/deck.vue
index 750cdca90e..623a109e88 100644
--- a/packages/frontend/src/ui/deck.vue
+++ b/packages/frontend/src/ui/deck.vue
@@ -332,7 +332,7 @@ body {
 	overflow-x: auto;
 	overflow-y: clip;
 	overscroll-behavior: contain;
-	background: var(--deckBg);
+	background: var(--MI_THEME-deckBg);
 
 	&.center {
 		> .section:first-of-type {
@@ -414,7 +414,7 @@ body {
 	contain: strict;
 	overflow: auto;
 	overscroll-behavior: contain;
-	background: var(--navBg);
+	background: var(--MI_THEME-navBg);
 }
 
 .nav {
@@ -430,8 +430,8 @@ body {
 	box-sizing: border-box;
 	-webkit-backdrop-filter: var(--blur, blur(32px));
 	backdrop-filter: var(--blur, blur(32px));
-	background-color: var(--header);
-	border-top: solid 0.5px var(--divider);
+	background-color: var(--MI_THEME-header);
+	border-top: solid 0.5px var(--MI_THEME-divider);
 }
 
 .navButton {
@@ -442,29 +442,29 @@ body {
 	max-width: 60px;
 	margin: auto;
 	border-radius: 100%;
-	background: var(--panel);
-	color: var(--fg);
+	background: var(--MI_THEME-panel);
+	color: var(--MI_THEME-fg);
 
 	&:hover {
-		background: var(--panelHighlight);
+		background: var(--MI_THEME-panelHighlight);
 	}
 
 	&:active {
-		background: hsl(from var(--panel) h s calc(l - 2));
+		background: hsl(from var(--MI_THEME-panel) h s calc(l - 2));
 	}
 }
 
 .postButton {
 	composes: navButton;
-	background: linear-gradient(90deg, var(--buttonGradateA), var(--buttonGradateB));
-	color: var(--fgOnAccent);
+	background: linear-gradient(90deg, var(--MI_THEME-buttonGradateA), var(--MI_THEME-buttonGradateB));
+	color: var(--MI_THEME-fgOnAccent);
 
 	&:hover {
-		background: linear-gradient(90deg, hsl(from var(--accent) h s calc(l + 5)), hsl(from var(--accent) h s calc(l + 5)));
+		background: linear-gradient(90deg, hsl(from var(--MI_THEME-accent) h s calc(l + 5)), hsl(from var(--MI_THEME-accent) h s calc(l + 5)));
 	}
 
 	&:active {
-		background: linear-gradient(90deg, hsl(from var(--accent) h s calc(l + 5)), hsl(from var(--accent) h s calc(l + 5)));
+		background: linear-gradient(90deg, hsl(from var(--MI_THEME-accent) h s calc(l + 5)), hsl(from var(--MI_THEME-accent) h s calc(l + 5)));
 	}
 }
 
@@ -477,7 +477,7 @@ body {
 	position: absolute;
 	top: 0;
 	left: 0;
-	color: var(--indicator);
+	color: var(--MI_THEME-indicator);
 	font-size: 16px;
 	animation: global-blink 1s infinite;
 
diff --git a/packages/frontend/src/ui/deck/column.vue b/packages/frontend/src/ui/deck/column.vue
index b97d86f4a3..4aaaea0fd9 100644
--- a/packages/frontend/src/ui/deck/column.vue
+++ b/packages/frontend/src/ui/deck/column.vue
@@ -21,7 +21,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 	>
 		<svg viewBox="0 0 256 128" :class="$style.tabShape">
 			<g transform="matrix(6.2431,0,0,6.2431,-677.417,-29.3839)">
-				<path d="M149.512,4.707L108.507,4.707C116.252,4.719 118.758,14.958 118.758,14.958C118.758,14.958 121.381,25.283 129.009,25.209L149.512,25.209L149.512,4.707Z" style="fill:var(--deckBg);"/>
+				<path d="M149.512,4.707L108.507,4.707C116.252,4.719 118.758,14.958 118.758,14.958C118.758,14.958 121.381,25.283 129.009,25.209L149.512,25.209L149.512,4.707Z" style="fill:var(--MI_THEME-deckBg);"/>
 			</g>
 		</svg>
 		<div :class="$style.color"></div>
@@ -299,7 +299,7 @@ function onDrop(ev) {
 			left: 0;
 			width: 100%;
 			height: 100%;
-			background: var(--focus);
+			background: var(--MI_THEME-focus);
 		}
 	}
 
@@ -313,7 +313,7 @@ function onDrop(ev) {
 			left: 0;
 			width: 100%;
 			height: 100%;
-			background: var(--focus);
+			background: var(--MI_THEME-focus);
 			opacity: 0.5;
 		}
 	}
@@ -331,19 +331,19 @@ function onDrop(ev) {
 	}
 
 	&.naked {
-		background: var(--acrylicBg) !important;
+		background: var(--MI_THEME-acrylicBg) !important;
 		-webkit-backdrop-filter: var(--blur, blur(10px));
 		backdrop-filter: var(--blur, blur(10px));
 
 		> .header {
 			background: transparent;
 			box-shadow: none;
-			color: var(--fg);
+			color: var(--MI_THEME-fg);
 		}
 
 		> .body {
 			background: transparent !important;
-			scrollbar-color: var(--scrollbarHandle) transparent;
+			scrollbar-color: var(--MI_THEME-scrollbarHandle) transparent;
 
 			&::-webkit-scrollbar-track {
 				background: transparent;
@@ -352,12 +352,12 @@ function onDrop(ev) {
 	}
 
 	&.paged {
-		background: var(--bg) !important;
+		background: var(--MI_THEME-bg) !important;
 
 		> .body {
-			background: var(--bg) !important;
+			background: var(--MI_THEME-bg) !important;
 			overflow-y: scroll !important;
-			scrollbar-color: var(--scrollbarHandle) transparent;
+			scrollbar-color: var(--MI_THEME-scrollbarHandle) transparent;
 
 			&::-webkit-scrollbar-track {
 				background: inherit;
@@ -374,9 +374,9 @@ function onDrop(ev) {
 	height: var(--deckColumnHeaderHeight);
 	padding: 0 16px 0 30px;
 	font-size: 0.9em;
-	color: var(--panelHeaderFg);
-	background: var(--panelHeaderBg);
-	box-shadow: 0 1px 0 0 var(--panelHeaderDivider);
+	color: var(--MI_THEME-panelHeaderFg);
+	background: var(--MI_THEME-panelHeaderBg);
+	box-shadow: 0 1px 0 0 var(--MI_THEME-panelHeaderDivider);
 	cursor: pointer;
 	user-select: none;
 }
@@ -387,7 +387,7 @@ function onDrop(ev) {
 	left: 12px;
 	width: 3px;
 	height: calc(100% - 24px);
-	background: var(--accent);
+	background: var(--MI_THEME-accent);
 	border-radius: 999px;
 }
 
@@ -441,11 +441,11 @@ function onDrop(ev) {
 	overscroll-behavior-y: contain;
 	box-sizing: border-box;
 	container-type: size;
-	background-color: var(--bg);
-	scrollbar-color: var(--scrollbarHandle) var(--panel);
+	background-color: var(--MI_THEME-bg);
+	scrollbar-color: var(--MI_THEME-scrollbarHandle) var(--MI_THEME-panel);
 
 	&::-webkit-scrollbar-track {
-		background: var(--panel);
+		background: var(--MI_THEME-panel);
 	}
 }
 </style>
diff --git a/packages/frontend/src/ui/deck/widgets-column.vue b/packages/frontend/src/ui/deck/widgets-column.vue
index 9995996771..da12570ae2 100644
--- a/packages/frontend/src/ui/deck/widgets-column.vue
+++ b/packages/frontend/src/ui/deck/widgets-column.vue
@@ -58,7 +58,7 @@ const menu = [{
 <style lang="scss" module>
 .root {
 	--margin: 8px;
-	--panelBorder: none;
+	--MI_THEME-panelBorder: none;
 
 	padding: 0 var(--margin);
 }
diff --git a/packages/frontend/src/ui/universal.vue b/packages/frontend/src/ui/universal.vue
index a2a79c74a1..73c4e7c195 100644
--- a/packages/frontend/src/ui/universal.vue
+++ b/packages/frontend/src/ui/universal.vue
@@ -318,7 +318,7 @@ $widgets-hide-threshold: 1090px;
 }
 
 .sidebar {
-	border-right: solid 0.5px var(--divider);
+	border-right: solid 0.5px var(--MI_THEME-divider);
 }
 
 .contents {
@@ -328,7 +328,7 @@ $widgets-hide-threshold: 1090px;
 	overflow: auto;
 	overflow-y: scroll;
 	overscroll-behavior: contain;
-	background: var(--bg);
+	background: var(--MI_THEME-bg);
 }
 
 .widgets {
@@ -337,8 +337,8 @@ $widgets-hide-threshold: 1090px;
 	box-sizing: border-box;
 	overflow: auto;
 	padding: var(--margin) var(--margin) calc(var(--margin) + env(safe-area-inset-bottom, 0px));
-	border-left: solid 0.5px var(--divider);
-	background: var(--bg);
+	border-left: solid 0.5px var(--MI_THEME-divider);
+	background: var(--MI_THEME-bg);
 
 	@media (max-width: $widgets-hide-threshold) {
 		display: none;
@@ -356,7 +356,7 @@ $widgets-hide-threshold: 1090px;
 	border-radius: 100%;
 	box-shadow: 0 3px 5px -1px rgba(0, 0, 0, 0.2), 0 6px 10px 0 rgba(0, 0, 0, 0.14), 0 1px 18px 0 rgba(0, 0, 0, 0.12);
 	font-size: 22px;
-	background: var(--panel);
+	background: var(--MI_THEME-panel);
 }
 
 .widgetsDrawerBg {
@@ -374,7 +374,7 @@ $widgets-hide-threshold: 1090px;
 	box-sizing: border-box;
 	overflow: auto;
 	overscroll-behavior: contain;
-	background: var(--bg);
+	background: var(--MI_THEME-bg);
 }
 
 .widgetsCloseButton {
@@ -402,8 +402,8 @@ $widgets-hide-threshold: 1090px;
 	box-sizing: border-box;
 	-webkit-backdrop-filter: var(--blur, blur(24px));
 	backdrop-filter: var(--blur, blur(24px));
-	background-color: var(--header);
-	border-top: solid 0.5px var(--divider);
+	background-color: var(--MI_THEME-header);
+	border-top: solid 0.5px var(--MI_THEME-divider);
 }
 
 .navButton {
@@ -414,29 +414,29 @@ $widgets-hide-threshold: 1090px;
 	max-width: 60px;
 	margin: auto;
 	border-radius: 100%;
-	background: var(--panel);
-	color: var(--fg);
+	background: var(--MI_THEME-panel);
+	color: var(--MI_THEME-fg);
 
 	&:hover {
-		background: var(--panelHighlight);
+		background: var(--MI_THEME-panelHighlight);
 	}
 
 	&:active {
-		background: hsl(from var(--panel) h s calc(l - 2));
+		background: hsl(from var(--MI_THEME-panel) h s calc(l - 2));
 	}
 }
 
 .postButton {
 	composes: navButton;
-	background: linear-gradient(90deg, var(--buttonGradateA), var(--buttonGradateB));
-	color: var(--fgOnAccent);
+	background: linear-gradient(90deg, var(--MI_THEME-buttonGradateA), var(--MI_THEME-buttonGradateB));
+	color: var(--MI_THEME-fgOnAccent);
 
 	&:hover {
-		background: linear-gradient(90deg, hsl(from var(--accent) h s calc(l + 5)), hsl(from var(--accent) h s calc(l + 5)));
+		background: linear-gradient(90deg, hsl(from var(--MI_THEME-accent) h s calc(l + 5)), hsl(from var(--MI_THEME-accent) h s calc(l + 5)));
 	}
 
 	&:active {
-		background: linear-gradient(90deg, hsl(from var(--accent) h s calc(l + 5)), hsl(from var(--accent) h s calc(l + 5)));
+		background: linear-gradient(90deg, hsl(from var(--MI_THEME-accent) h s calc(l + 5)), hsl(from var(--MI_THEME-accent) h s calc(l + 5)));
 	}
 }
 
@@ -449,7 +449,7 @@ $widgets-hide-threshold: 1090px;
 	position: absolute;
 	top: 0;
 	left: 0;
-	color: var(--indicator);
+	color: var(--MI_THEME-indicator);
 	font-size: 16px;
 	animation: global-blink 1s infinite;
 
@@ -474,7 +474,7 @@ $widgets-hide-threshold: 1090px;
 	contain: strict;
 	overflow: auto;
 	overscroll-behavior: contain;
-	background: var(--navBg);
+	background: var(--MI_THEME-navBg);
 }
 
 .statusbars {
diff --git a/packages/frontend/src/ui/visitor.vue b/packages/frontend/src/ui/visitor.vue
index 01d0737123..7d8677e3be 100644
--- a/packages/frontend/src/ui/visitor.vue
+++ b/packages/frontend/src/ui/visitor.vue
@@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <template>
 <div class="mk-app">
-	<a v-if="isRoot" href="https://github.com/misskey-dev/misskey" target="_blank" class="github-corner" aria-label="View source on GitHub"><svg width="80" height="80" viewBox="0 0 250 250" style="fill:var(--panel); color:var(--fg); position: fixed; z-index: 10; top: 0; border: 0; right: 0;" aria-hidden="true"><path d="M0,0 L115,115 L130,115 L142,142 L250,250 L250,0 Z"></path><path d="M128.3,109.0 C113.8,99.7 119.0,89.6 119.0,89.6 C122.0,82.7 120.5,78.6 120.5,78.6 C119.2,72.0 123.4,76.3 123.4,76.3 C127.3,80.9 125.5,87.3 125.5,87.3 C122.9,97.6 130.6,101.9 134.4,103.2" fill="currentColor" style="transform-origin: 130px 106px;" class="octo-arm"></path><path d="M115.0,115.0 C114.9,115.1 118.7,116.5 119.8,115.4 L133.7,101.6 C136.9,99.2 139.9,98.4 142.2,98.6 C133.8,88.0 127.5,74.4 143.8,58.0 C148.5,53.4 154.0,51.2 159.7,51.0 C160.3,49.4 163.2,43.6 171.4,40.1 C171.4,40.1 176.1,42.5 178.8,56.2 C183.1,58.6 187.2,61.8 190.9,65.4 C194.5,69.0 197.7,73.2 200.1,77.6 C213.8,80.2 216.3,84.9 216.3,84.9 C212.7,93.1 206.9,96.0 205.4,96.6 C205.1,102.4 203.0,107.8 198.3,112.5 C181.9,128.9 168.3,122.5 157.7,114.1 C157.9,116.9 156.7,120.9 152.7,124.9 L141.0,136.5 C139.8,137.7 141.6,141.9 141.8,141.8 Z" fill="currentColor" class="octo-body"></path></svg></a>
+	<a v-if="isRoot" href="https://github.com/misskey-dev/misskey" target="_blank" class="github-corner" aria-label="View source on GitHub"><svg width="80" height="80" viewBox="0 0 250 250" style="fill:var(--MI_THEME-panel); color:var(--MI_THEME-fg); position: fixed; z-index: 10; top: 0; border: 0; right: 0;" aria-hidden="true"><path d="M0,0 L115,115 L130,115 L142,142 L250,250 L250,0 Z"></path><path d="M128.3,109.0 C113.8,99.7 119.0,89.6 119.0,89.6 C122.0,82.7 120.5,78.6 120.5,78.6 C119.2,72.0 123.4,76.3 123.4,76.3 C127.3,80.9 125.5,87.3 125.5,87.3 C122.9,97.6 130.6,101.9 134.4,103.2" fill="currentColor" style="transform-origin: 130px 106px;" class="octo-arm"></path><path d="M115.0,115.0 C114.9,115.1 118.7,116.5 119.8,115.4 L133.7,101.6 C136.9,99.2 139.9,98.4 142.2,98.6 C133.8,88.0 127.5,74.4 143.8,58.0 C148.5,53.4 154.0,51.2 159.7,51.0 C160.3,49.4 163.2,43.6 171.4,40.1 C171.4,40.1 176.1,42.5 178.8,56.2 C183.1,58.6 187.2,61.8 190.9,65.4 C194.5,69.0 197.7,73.2 200.1,77.6 C213.8,80.2 216.3,84.9 216.3,84.9 C212.7,93.1 206.9,96.0 205.4,96.6 C205.1,102.4 203.0,107.8 198.3,112.5 C181.9,128.9 168.3,122.5 157.7,114.1 C157.9,116.9 156.7,120.9 152.7,124.9 L141.0,136.5 C139.8,137.7 141.6,141.9 141.8,141.8 Z" fill="currentColor" class="octo-body"></path></svg></a>
 
 	<div v-if="!narrow && !isRoot" class="side">
 		<div class="banner" :style="{ backgroundImage: instance.backgroundImageUrl ? `url(${ instance.backgroundImageUrl })` : 'none' }"></div>
@@ -191,7 +191,7 @@ defineExpose({
 		left: 0;
 		width: 500px;
 		height: 100vh;
-		background: var(--accent);
+		background: var(--MI_THEME-accent);
 
 		> .banner {
 			position: absolute;
@@ -219,7 +219,7 @@ defineExpose({
 		min-width: 0;
 
 		> .header {
-			background: var(--panel);
+			background: var(--MI_THEME-panel);
 
 			> .wide {
 				line-height: 50px;
@@ -254,7 +254,7 @@ defineExpose({
 		left: 0;
 		width: 240px;
 		height: 100vh;
-		background: var(--panel);
+		background: var(--MI_THEME-panel);
 
 		> .link {
 			display: block;
@@ -268,7 +268,7 @@ defineExpose({
 		> .divider {
 			margin: 8px auto;
 			width: calc(100% - 32px);
-			border-top: solid 0.5px var(--divider);
+			border-top: solid 0.5px var(--MI_THEME-divider);
 		}
 
 		> .action {
@@ -283,7 +283,7 @@ defineExpose({
 				border-radius: 999px;
 
 				&._button {
-					background: var(--panel);
+					background: var(--MI_THEME-panel);
 				}
 
 				&:first-child {
diff --git a/packages/frontend/src/ui/zen.vue b/packages/frontend/src/ui/zen.vue
index f22bf41fd7..93d57b647e 100644
--- a/packages/frontend/src/ui/zen.vue
+++ b/packages/frontend/src/ui/zen.vue
@@ -81,8 +81,8 @@ document.documentElement.style.overflowY = 'scroll';
 	max-width: 60px;
 	margin: auto;
 	border-radius: 100%;
-	background: var(--panel);
-	color: var(--fg);
+	background: var(--MI_THEME-panel);
+	color: var(--MI_THEME-fg);
 	right: var(--margin);
 	bottom: calc(var(--margin) + env(safe-area-inset-bottom, 0px));
 }
diff --git a/packages/frontend/src/widgets/WidgetAiscript.vue b/packages/frontend/src/widgets/WidgetAiscript.vue
index a74483e85e..cf7e9c5a3e 100644
--- a/packages/frontend/src/widgets/WidgetAiscript.vue
+++ b/packages/frontend/src/widgets/WidgetAiscript.vue
@@ -126,10 +126,10 @@ defineExpose<WidgetComponentExpose>({
 		max-width: 100%;
 		min-width: 100%;
 		padding: 16px;
-		color: var(--fg);
+		color: var(--MI_THEME-fg);
 		background: transparent;
 		border: none;
-		border-bottom: solid 0.5px var(--divider);
+		border-bottom: solid 0.5px var(--MI_THEME-divider);
 		border-radius: 0;
 		box-sizing: border-box;
 		font: inherit;
@@ -154,7 +154,7 @@ defineExpose<WidgetComponentExpose>({
 	}
 
 	> .logs {
-		border-top: solid 0.5px var(--divider);
+		border-top: solid 0.5px var(--MI_THEME-divider);
 		text-align: left;
 		padding: 16px;
 
diff --git a/packages/frontend/src/widgets/WidgetCalendar.vue b/packages/frontend/src/widgets/WidgetCalendar.vue
index 412d527819..2443e40789 100644
--- a/packages/frontend/src/widgets/WidgetCalendar.vue
+++ b/packages/frontend/src/widgets/WidgetCalendar.vue
@@ -207,7 +207,7 @@ defineExpose<WidgetComponentExpose>({
 .meter {
 	width: 100%;
 	overflow: hidden;
-	background: var(--X11);
+	background: var(--MI_THEME-X11);
 	border-radius: 8px;
 }
 
diff --git a/packages/frontend/src/widgets/WidgetFederation.vue b/packages/frontend/src/widgets/WidgetFederation.vue
index c10416e4fb..4a10a612e2 100644
--- a/packages/frontend/src/widgets/WidgetFederation.vue
+++ b/packages/frontend/src/widgets/WidgetFederation.vue
@@ -105,7 +105,7 @@ defineExpose<WidgetComponentExpose>({
 			display: flex;
 			align-items: center;
 			padding: 14px 16px;
-			border-bottom: solid 0.5px var(--divider);
+			border-bottom: solid 0.5px var(--MI_THEME-divider);
 
 			> img {
 				display: block;
@@ -120,7 +120,7 @@ defineExpose<WidgetComponentExpose>({
 				flex: 1;
 				overflow: hidden;
 				font-size: 0.9em;
-				color: var(--fg);
+				color: var(--MI_THEME-fg);
 				padding-right: 8px;
 
 				> .a {
diff --git a/packages/frontend/src/widgets/WidgetJobQueue.vue b/packages/frontend/src/widgets/WidgetJobQueue.vue
index edf6622a13..0ee6b863dc 100644
--- a/packages/frontend/src/widgets/WidgetJobQueue.vue
+++ b/packages/frontend/src/widgets/WidgetJobQueue.vue
@@ -173,14 +173,14 @@ defineExpose<WidgetComponentExpose>({
 		padding: 16px;
 
 		&:not(:first-child) {
-			border-top: solid 0.5px var(--divider);
+			border-top: solid 0.5px var(--MI_THEME-divider);
 		}
 
 		> .label {
 			display: flex;
 
 			> .icon {
-				color: var(--warn);
+				color: var(--MI_THEME-warn);
 				margin-left: auto;
 				animation: warnBlink 1s infinite;
 			}
@@ -198,11 +198,11 @@ defineExpose<WidgetComponentExpose>({
 
 				> div:last-child {
 					&.inc {
-						color: var(--warn);
+						color: var(--MI_THEME-warn);
 					}
 
 					&.dec {
-						color: var(--success);
+						color: var(--MI_THEME-success);
 					}
 				}
 			}
diff --git a/packages/frontend/src/widgets/WidgetMemo.vue b/packages/frontend/src/widgets/WidgetMemo.vue
index 7ee83157c6..7b179ce703 100644
--- a/packages/frontend/src/widgets/WidgetMemo.vue
+++ b/packages/frontend/src/widgets/WidgetMemo.vue
@@ -84,10 +84,10 @@ defineExpose<WidgetComponentExpose>({
 	max-width: 100%;
 	min-width: 100%;
 	padding: 16px;
-	color: var(--fg);
+	color: var(--MI_THEME-fg);
 	background: transparent;
 	border: none;
-	border-bottom: solid 0.5px var(--divider);
+	border-bottom: solid 0.5px var(--MI_THEME-divider);
 	border-radius: 0;
 	box-sizing: border-box;
 	font: inherit;
diff --git a/packages/frontend/src/widgets/WidgetOnlineUsers.vue b/packages/frontend/src/widgets/WidgetOnlineUsers.vue
index d56ee96ac1..d8c4e259c8 100644
--- a/packages/frontend/src/widgets/WidgetOnlineUsers.vue
+++ b/packages/frontend/src/widgets/WidgetOnlineUsers.vue
@@ -72,6 +72,6 @@ defineExpose<WidgetComponentExpose>({
 }
 
 .text {
-	color: var(--fgTransparentWeak);
+	color: var(--MI_THEME-fgTransparentWeak);
 }
 </style>
diff --git a/packages/frontend/src/widgets/WidgetRss.vue b/packages/frontend/src/widgets/WidgetRss.vue
index 511777a570..3e43687709 100644
--- a/packages/frontend/src/widgets/WidgetRss.vue
+++ b/packages/frontend/src/widgets/WidgetRss.vue
@@ -113,7 +113,7 @@ defineExpose<WidgetComponentExpose>({
 .item {
 	display: block;
 	padding: 8px 16px;
-	color: var(--fg);
+	color: var(--MI_THEME-fg);
 	white-space: nowrap;
 	text-overflow: ellipsis;
 	overflow: hidden;
diff --git a/packages/frontend/src/widgets/WidgetRssTicker.vue b/packages/frontend/src/widgets/WidgetRssTicker.vue
index b393ecd74b..4f594b720f 100644
--- a/packages/frontend/src/widgets/WidgetRssTicker.vue
+++ b/packages/frontend/src/widgets/WidgetRssTicker.vue
@@ -171,7 +171,7 @@ defineExpose<WidgetComponentExpose>({
 	display: inline-flex;
 	align-items: center;
 	vertical-align: bottom;
-	color: var(--fg);
+	color: var(--MI_THEME-fg);
 }
 
 .divider {
@@ -179,6 +179,6 @@ defineExpose<WidgetComponentExpose>({
 	width: 0.5px;
 	height: 16px;
 	margin: 0 1em;
-	background: var(--divider);
+	background: var(--MI_THEME-divider);
 }
 </style>
diff --git a/packages/frontend/src/widgets/WidgetTrends.vue b/packages/frontend/src/widgets/WidgetTrends.vue
index a41db513e8..47a4efc106 100644
--- a/packages/frontend/src/widgets/WidgetTrends.vue
+++ b/packages/frontend/src/widgets/WidgetTrends.vue
@@ -91,13 +91,13 @@ defineExpose<WidgetComponentExpose>({
 			display: flex;
 			align-items: center;
 			padding: 14px 16px;
-			border-bottom: solid 0.5px var(--divider);
+			border-bottom: solid 0.5px var(--MI_THEME-divider);
 
 			> .tag {
 				flex: 1;
 				overflow: hidden;
 				font-size: 0.9em;
-				color: var(--fg);
+				color: var(--MI_THEME-fg);
 
 				> .a {
 					display: block;

From a624546812af072d23579bce81f85668f9a97c09 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=8B=E3=81=A3=E3=81=93=E3=81=8B=E3=82=8A?=
 <67428053+kakkokari-gtyih@users.noreply.github.com>
Date: Thu, 10 Oct 2024 14:05:20 +0900
Subject: [PATCH 071/121] =?UTF-8?q?fix(frontend):=20=E3=83=A6=E3=83=BC?=
 =?UTF-8?q?=E3=82=B6=E3=83=BC=E7=99=BB=E9=8C=B2=E5=AE=8C=E4=BA=86=E6=99=82?=
 =?UTF-8?q?=E3=81=AB=E3=82=B5=E3=82=A4=E3=83=B3=E3=82=A4=E3=83=B3API?=
 =?UTF-8?q?=E3=82=92=E5=88=A5=E9=80=94=E4=BD=BF=E7=94=A8=E3=81=97=E3=81=A6?=
 =?UTF-8?q?=E3=81=84=E3=81=9F=E3=81=AE=E3=82=92=E4=BF=AE=E6=AD=A3=20(#1473?=
 =?UTF-8?q?8)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

* fix(frontend): ユーザー登録完了時にサインインAPIを別途使用していたのを修正

* emitされるオブジェクトの型を変更したことに伴う修正

* Update Changelog
---
 CHANGELOG.md                                  |  2 +-
 packages/frontend/src/account.ts              |  8 +-
 packages/frontend/src/components/MkSignin.vue |  4 +-
 .../src/components/MkSigninDialog.vue         |  5 +-
 .../src/components/MkSignupDialog.form.vue    | 82 +++++++++++--------
 .../src/components/MkSignupDialog.vue         |  4 +-
 .../src/components/MkUserCardMini.vue         |  2 +-
 .../frontend/src/pages/settings/accounts.vue  | 18 ++--
 packages/misskey-js/etc/misskey-js.api.md     |  4 +-
 packages/misskey-js/src/entities.ts           |  2 +-
 10 files changed, 72 insertions(+), 59 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index e8909c15da..36645aff74 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,7 +4,7 @@
 -
 
 ### Client
--
+- メールアドレス不要でCaptchaが有効な場合にアカウント登録完了後自動でのログインに失敗する問題を修正
 
 ### Server
 -
diff --git a/packages/frontend/src/account.ts b/packages/frontend/src/account.ts
index 84d89b1b3f..b91834b94f 100644
--- a/packages/frontend/src/account.ts
+++ b/packages/frontend/src/account.ts
@@ -226,7 +226,7 @@ export async function openAccountMenu(opts: {
 
 	function showSigninDialog() {
 		const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkSigninDialog.vue')), {}, {
-			done: res => {
+			done: (res: Misskey.entities.SigninFlowResponse & { finished: true }) => {
 				addAccount(res.id, res.i);
 				success();
 			},
@@ -236,9 +236,9 @@ export async function openAccountMenu(opts: {
 
 	function createAccount() {
 		const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkSignupDialog.vue')), {}, {
-			done: res => {
-				addAccount(res.id, res.i);
-				switchAccountWithToken(res.i);
+			done: (res: Misskey.entities.SignupResponse) => {
+				addAccount(res.id, res.token);
+				switchAccountWithToken(res.token);
 			},
 			closed: () => dispose(),
 		});
diff --git a/packages/frontend/src/components/MkSignin.vue b/packages/frontend/src/components/MkSignin.vue
index a79d7cf07a..a773cefdab 100644
--- a/packages/frontend/src/components/MkSignin.vue
+++ b/packages/frontend/src/components/MkSignin.vue
@@ -83,7 +83,7 @@ import type { AuthenticationPublicKeyCredential } from '@github/webauthn-json/br
 import type { OpenOnRemoteOptions } from '@/scripts/please-login.js';
 
 const emit = defineEmits<{
-	(ev: 'login', v: Misskey.entities.SigninFlowResponse): void;
+	(ev: 'login', v: Misskey.entities.SigninFlowResponse & { finished: true }): void;
 }>();
 
 const props = withDefaults(defineProps<{
@@ -276,7 +276,7 @@ async function tryLogin(req: Partial<Misskey.entities.SigninFlowRequest>): Promi
 	});
 }
 
-async function onLoginSucceeded(res: Misskey.entities.SigninFlowResponse & { finished: true; }) {
+async function onLoginSucceeded(res: Misskey.entities.SigninFlowResponse & { finished: true }) {
 	if (props.autoSet) {
 		await login(res.i);
 	}
diff --git a/packages/frontend/src/components/MkSigninDialog.vue b/packages/frontend/src/components/MkSigninDialog.vue
index 2aa11ac319..51dea960aa 100644
--- a/packages/frontend/src/components/MkSigninDialog.vue
+++ b/packages/frontend/src/components/MkSigninDialog.vue
@@ -23,6 +23,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
+import * as Misskey from 'misskey-js';
 import { shallowRef } from 'vue';
 import type { OpenOnRemoteOptions } from '@/scripts/please-login.js';
 import MkSignin from '@/components/MkSignin.vue';
@@ -40,7 +41,7 @@ withDefaults(defineProps<{
 });
 
 const emit = defineEmits<{
-	(ev: 'done', v: any): void;
+	(ev: 'done', v: Misskey.entities.SigninFlowResponse & { finished: true }): void;
 	(ev: 'closed'): void;
 	(ev: 'cancelled'): void;
 }>();
@@ -52,7 +53,7 @@ function onClose() {
 	if (modal.value) modal.value.close();
 }
 
-function onLogin(res) {
+function onLogin(res: Misskey.entities.SigninFlowResponse & { finished: true }) {
 	emit('done', res);
 	if (modal.value) modal.value.close();
 }
diff --git a/packages/frontend/src/components/MkSignupDialog.form.vue b/packages/frontend/src/components/MkSignupDialog.form.vue
index a0c5488983..ffb5551ff3 100644
--- a/packages/frontend/src/components/MkSignupDialog.form.vue
+++ b/packages/frontend/src/components/MkSignupDialog.form.vue
@@ -98,7 +98,7 @@ const props = withDefaults(defineProps<{
 });
 
 const emit = defineEmits<{
-	(ev: 'signup', user: Misskey.entities.SigninFlowResponse): void;
+	(ev: 'signup', user: Misskey.entities.SignupResponse): void;
 	(ev: 'signupEmailPending'): void;
 }>();
 
@@ -250,18 +250,30 @@ async function onSubmit(): Promise<void> {
 	if (submitting.value) return;
 	submitting.value = true;
 
-	try {
-		await misskeyApi('signup', {
-			username: username.value,
-			password: password.value,
-			emailAddress: email.value,
-			invitationCode: invitationCode.value,
-			'hcaptcha-response': hCaptchaResponse.value,
-			'm-captcha-response': mCaptchaResponse.value,
-			'g-recaptcha-response': reCaptchaResponse.value,
-			'turnstile-response': turnstileResponse.value,
-		});
-		if (instance.emailRequiredForSignup) {
+	const signupPayload: Misskey.entities.SignupRequest = {
+		username: username.value,
+		password: password.value,
+		emailAddress: email.value,
+		invitationCode: invitationCode.value,
+		'hcaptcha-response': hCaptchaResponse.value,
+		'm-captcha-response': mCaptchaResponse.value,
+		'g-recaptcha-response': reCaptchaResponse.value,
+		'turnstile-response': turnstileResponse.value,
+	};
+
+	const res = await fetch(`${config.apiUrl}/signup`, {
+		method: 'POST',
+		headers: {
+			'Content-Type': 'application/json',
+		},
+		body: JSON.stringify(signupPayload),
+	}).catch(() => {
+		onSignupApiError();
+		return null;
+	});
+
+	if (res) {
+		if (res.status === 204 || instance.emailRequiredForSignup) {
 			os.alert({
 				type: 'success',
 				title: i18n.ts._signup.almostThere,
@@ -269,33 +281,31 @@ async function onSubmit(): Promise<void> {
 			});
 			emit('signupEmailPending');
 		} else {
-			const res = await misskeyApi('signin-flow', {
-				username: username.value,
-				password: password.value,
-			});
-			emit('signup', res);
+			const resJson = (await res.json()) as Misskey.entities.SignupResponse;
+			if (_DEV_) console.log(resJson);
 
-			if (props.autoSet && res.finished) {
-				return login(res.i);
-			} else {
-				os.alert({
-					type: 'error',
-					text: i18n.ts.somethingHappened,
-				});
+			emit('signup', resJson);
+
+			if (props.autoSet) {
+				await login(resJson.token);
 			}
 		}
-	} catch {
-		submitting.value = false;
-		hcaptcha.value?.reset?.();
-		mcaptcha.value?.reset?.();
-		recaptcha.value?.reset?.();
-		turnstile.value?.reset?.();
-
-		os.alert({
-			type: 'error',
-			text: i18n.ts.somethingHappened,
-		});
 	}
+
+	submitting.value = false;
+}
+
+function onSignupApiError() {
+	submitting.value = false;
+	hcaptcha.value?.reset?.();
+	mcaptcha.value?.reset?.();
+	recaptcha.value?.reset?.();
+	turnstile.value?.reset?.();
+
+	os.alert({
+		type: 'error',
+		text: i18n.ts.somethingHappened,
+	});
 }
 </script>
 
diff --git a/packages/frontend/src/components/MkSignupDialog.vue b/packages/frontend/src/components/MkSignupDialog.vue
index 4cccd99492..f240e6dc46 100644
--- a/packages/frontend/src/components/MkSignupDialog.vue
+++ b/packages/frontend/src/components/MkSignupDialog.vue
@@ -47,7 +47,7 @@ const props = withDefaults(defineProps<{
 });
 
 const emit = defineEmits<{
-	(ev: 'done', res: Misskey.entities.SigninFlowResponse): void;
+	(ev: 'done', res: Misskey.entities.SignupResponse): void;
 	(ev: 'closed'): void;
 }>();
 
@@ -55,7 +55,7 @@ const dialog = shallowRef<InstanceType<typeof MkModalWindow>>();
 
 const isAcceptedServerRule = ref(false);
 
-function onSignup(res: Misskey.entities.SigninFlowResponse) {
+function onSignup(res: Misskey.entities.SignupResponse) {
 	emit('done', res);
 	dialog.value?.close();
 }
diff --git a/packages/frontend/src/components/MkUserCardMini.vue b/packages/frontend/src/components/MkUserCardMini.vue
index b333722dc2..7a2e878931 100644
--- a/packages/frontend/src/components/MkUserCardMini.vue
+++ b/packages/frontend/src/components/MkUserCardMini.vue
@@ -23,7 +23,7 @@ import { acct } from '@/filters/user.js';
 
 const props = withDefaults(defineProps<{
 	user: Misskey.entities.User;
-	withChart: boolean;
+	withChart?: boolean;
 }>(), {
 	withChart: true,
 });
diff --git a/packages/frontend/src/pages/settings/accounts.vue b/packages/frontend/src/pages/settings/accounts.vue
index 08c9261dcf..1bbedb817e 100644
--- a/packages/frontend/src/pages/settings/accounts.vue
+++ b/packages/frontend/src/pages/settings/accounts.vue
@@ -45,7 +45,7 @@ const init = async () => {
 	});
 };
 
-function menu(account, ev) {
+function menu(account: Misskey.entities.UserDetailed, ev: MouseEvent) {
 	os.popupMenu([{
 		text: i18n.ts.switch,
 		icon: 'ti ti-switch-horizontal',
@@ -58,7 +58,7 @@ function menu(account, ev) {
 	}], ev.currentTarget ?? ev.target);
 }
 
-function addAccount(ev) {
+function addAccount(ev: MouseEvent) {
 	os.popupMenu([{
 		text: i18n.ts.existingAccount,
 		action: () => { addExistingAccount(); },
@@ -68,14 +68,14 @@ function addAccount(ev) {
 	}], ev.currentTarget ?? ev.target);
 }
 
-async function removeAccount(account) {
+async function removeAccount(account: Misskey.entities.UserDetailed) {
 	await _removeAccount(account.id);
 	accounts.value = accounts.value.filter(x => x.id !== account.id);
 }
 
 function addExistingAccount() {
 	const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkSigninDialog.vue')), {}, {
-		done: async res => {
+		done: async (res: Misskey.entities.SigninFlowResponse & { finished: true }) => {
 			await addAccounts(res.id, res.i);
 			os.success();
 			init();
@@ -86,17 +86,17 @@ function addExistingAccount() {
 
 function createAccount() {
 	const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkSignupDialog.vue')), {}, {
-		done: async res => {
-			await addAccounts(res.id, res.i);
-			switchAccountWithToken(res.i);
+		done: async (res: Misskey.entities.SignupResponse) => {
+			await addAccounts(res.id, res.token);
+			switchAccountWithToken(res.token);
 		},
 		closed: () => dispose(),
 	});
 }
 
 async function switchAccount(account: any) {
-	const fetchedAccounts: any[] = await getAccounts();
-	const token = fetchedAccounts.find(x => x.id === account.id).token;
+	const fetchedAccounts = await getAccounts();
+	const token = fetchedAccounts.find(x => x.id === account.id)!.token;
 	switchAccountWithToken(token);
 }
 
diff --git a/packages/misskey-js/etc/misskey-js.api.md b/packages/misskey-js/etc/misskey-js.api.md
index 1da8e4e613..72c236373d 100644
--- a/packages/misskey-js/etc/misskey-js.api.md
+++ b/packages/misskey-js/etc/misskey-js.api.md
@@ -3095,7 +3095,9 @@ type SigninWithPasskeyRequest = {
 
 // @public (undocumented)
 type SigninWithPasskeyResponse = {
-    signinResponse: SigninFlowResponse;
+    signinResponse: SigninFlowResponse & {
+        finished: true;
+    };
 };
 
 // @public (undocumented)
diff --git a/packages/misskey-js/src/entities.ts b/packages/misskey-js/src/entities.ts
index 2ffee40fba..dd88791ed0 100644
--- a/packages/misskey-js/src/entities.ts
+++ b/packages/misskey-js/src/entities.ts
@@ -308,7 +308,7 @@ export type SigninWithPasskeyInitResponse = {
 };
 
 export type SigninWithPasskeyResponse = {
-	signinResponse: SigninFlowResponse;
+	signinResponse: SigninFlowResponse & { finished: true };
 };
 
 type Values<T extends Record<PropertyKey, unknown>> = T[keyof T];

From 433732bcfc5ce6e8749463c1a2e216306b78d786 Mon Sep 17 00:00:00 2001
From: syuilo <4439005+syuilo@users.noreply.github.com>
Date: Thu, 10 Oct 2024 14:16:24 +0900
Subject: [PATCH 072/121] New Crowdin updates (#14733)

* New translations ja-jp.yml (English)

* New translations ja-jp.yml (Portuguese)

* New translations ja-jp.yml (Catalan)

* New translations ja-jp.yml (Italian)

* New translations ja-jp.yml (Russian)

* New translations ja-jp.yml (Chinese Traditional)

* New translations ja-jp.yml (Indonesian)

* New translations ja-jp.yml (French)

* New translations ja-jp.yml (Spanish)

* New translations ja-jp.yml (Arabic)

* New translations ja-jp.yml (Czech)

* New translations ja-jp.yml (German)

* New translations ja-jp.yml (Korean)

* New translations ja-jp.yml (Polish)

* New translations ja-jp.yml (Slovak)

* New translations ja-jp.yml (Ukrainian)

* New translations ja-jp.yml (Chinese Simplified)

* New translations ja-jp.yml (Vietnamese)

* New translations ja-jp.yml (Bengali)

* New translations ja-jp.yml (Thai)

* New translations ja-jp.yml (Japanese, Kansai)

* New translations ja-jp.yml (Catalan)
---
 locales/ar-SA.yml |  1 -
 locales/bn-BD.yml |  1 -
 locales/ca-ES.yml | 12 +++++++++++-
 locales/cs-CZ.yml |  1 -
 locales/de-DE.yml |  1 -
 locales/en-US.yml |  1 -
 locales/es-ES.yml |  1 -
 locales/fr-FR.yml |  1 -
 locales/id-ID.yml |  1 -
 locales/it-IT.yml |  1 -
 locales/ja-KS.yml |  1 -
 locales/ko-KR.yml |  1 -
 locales/pl-PL.yml |  1 -
 locales/pt-PT.yml |  3 +--
 locales/ru-RU.yml |  1 -
 locales/sk-SK.yml |  1 -
 locales/th-TH.yml |  1 -
 locales/uk-UA.yml |  1 -
 locales/vi-VN.yml |  1 -
 locales/zh-CN.yml |  1 -
 locales/zh-TW.yml |  1 -
 21 files changed, 12 insertions(+), 22 deletions(-)

diff --git a/locales/ar-SA.yml b/locales/ar-SA.yml
index d95600cb1f..de24ad4bb9 100644
--- a/locales/ar-SA.yml
+++ b/locales/ar-SA.yml
@@ -1252,7 +1252,6 @@ _theme:
     buttonBg: "خلفية الأزرار"
     buttonHoverBg: "خلفية الأزرار (عند التمرير فوقها)"
     inputBorder: "حواف حقل الإدخال"
-    listItemHoverBg: "خلفية عناصر القائمة (عند التمرير فوقها)"
     driveFolderBg: "خلفية مجلد قرص التخزين"
     messageBg: "خلفية المحادثة"
 _sfx:
diff --git a/locales/bn-BD.yml b/locales/bn-BD.yml
index ab0ee74bb4..0e761b0743 100644
--- a/locales/bn-BD.yml
+++ b/locales/bn-BD.yml
@@ -1017,7 +1017,6 @@ _theme:
     buttonBg: "বাটনের পটভূমি"
     buttonHoverBg: "বাটনের পটভূমি (হভার)"
     inputBorder: "ইনপুট ফিল্ডের বর্ডার"
-    listItemHoverBg: "লিস্ট আইটেমের পটভূমি (হোভার)"
     driveFolderBg: "ড্রাইভ ফোল্ডারের পটভূমি"
     wallpaperOverlay: "ওয়ালপেপার ওভারলে"
     badge: "ব্যাজ"
diff --git a/locales/ca-ES.yml b/locales/ca-ES.yml
index ad5fde37bc..7b668e5ce9 100644
--- a/locales/ca-ES.yml
+++ b/locales/ca-ES.yml
@@ -453,6 +453,7 @@ totpDescription: "Escriu una contrasenya d'un sol us fent servir l'aplicació d'
 moderator: "Moderador/a"
 moderation: "Moderació"
 moderationNote: "Nota de moderació "
+moderationNoteDescription: "Pots escriure notes que es compartiran entre els moderadors."
 addModerationNote: "Afegir una nota de moderació "
 moderationLogs: "Registre de moderació "
 nUsersMentioned: "{n} usuaris mencionats"
@@ -1284,6 +1285,14 @@ unknownWebAuthnKey: "Passkey desconeguda"
 passkeyVerificationFailed: "La verificació a fallat"
 passkeyVerificationSucceededButPasswordlessLoginDisabled: "La verificació de la passkey a estat correcta, però s'ha deshabilitat l'inici de sessió sense contrasenya."
 messageToFollower: "Missatge als meus seguidors"
+target: "Assumpte "
+_abuseUserReport:
+  forward: "Reenviar "
+  forwardDescription: "Reenvia l'informe a una altra instància com un compte del sistema anònima."
+  resolve: "Solució "
+  accept: "Acceptar "
+  reject: "Rebutjar"
+  resolveTutorial: "Si l'informe és legítim selecciona \"Acceptar\" per resoldre'l positivament. Però si l'informe no és legítim selecciona \"Rebutjar\" per resoldre'l negativament."
 _delivery:
   status: "Estat d'entrega "
   stop: "Suspés"
@@ -1974,7 +1983,6 @@ _theme:
     buttonBg: "Fons botó "
     buttonHoverBg: "Fons botó (en passar-hi per sobre)"
     inputBorder: "Contorn del cap d'introducció "
-    listItemHoverBg: "Fons dels elements d'una llista"
     driveFolderBg: "Fons de la carpeta Disc"
     wallpaperOverlay: "Superposició del fons de pantalla "
     badge: "Insígnia "
@@ -2520,6 +2528,8 @@ _moderationLogTypes:
   markSensitiveDriveFile: "Fitxer marcat com a sensible"
   unmarkSensitiveDriveFile: "S'ha tret la marca de sensible del fitxer"
   resolveAbuseReport: "Informe resolt"
+  forwardAbuseReport: "Informe reenviat"
+  updateAbuseReportNote: "Nota de moderació d'un informe actualitzat"
   createInvitation: "Crear codi d'invitació "
   createAd: "Anunci creat"
   deleteAd: "Anunci esborrat"
diff --git a/locales/cs-CZ.yml b/locales/cs-CZ.yml
index 4233a68f17..caf6d6e163 100644
--- a/locales/cs-CZ.yml
+++ b/locales/cs-CZ.yml
@@ -1629,7 +1629,6 @@ _theme:
     buttonBg: "Pozadí tlačítka"
     buttonHoverBg: "Pozadí tlačítka (Hover)"
     inputBorder: "Ohraničení vstupního pole"
-    listItemHoverBg: "Pozadí položky seznamu (Hover)"
     driveFolderBg: "Pozadí složky disku"
     wallpaperOverlay: "Překrytí tapety"
     badge: "Odznak"
diff --git a/locales/de-DE.yml b/locales/de-DE.yml
index 35a04b453c..4e2bd06934 100644
--- a/locales/de-DE.yml
+++ b/locales/de-DE.yml
@@ -1784,7 +1784,6 @@ _theme:
     buttonBg: "Hintergrund von Schaltflächen"
     buttonHoverBg: "Hintergrund von Schaltflächen (Mouseover)"
     inputBorder: "Rahmen von Eingabefeldern"
-    listItemHoverBg: "Hintergrund von Listeneinträgen (Mouseover)"
     driveFolderBg: "Hintergrund von Drive-Ordnern"
     wallpaperOverlay: "Hintergrundbild-Overlay"
     badge: "Wappen"
diff --git a/locales/en-US.yml b/locales/en-US.yml
index 126f769644..6ea7fb4f8d 100644
--- a/locales/en-US.yml
+++ b/locales/en-US.yml
@@ -1984,7 +1984,6 @@ _theme:
     buttonBg: "Button background"
     buttonHoverBg: "Button background (Hover)"
     inputBorder: "Input field border"
-    listItemHoverBg: "List item background (Hover)"
     driveFolderBg: "Drive folder background"
     wallpaperOverlay: "Wallpaper overlay"
     badge: "Badge"
diff --git a/locales/es-ES.yml b/locales/es-ES.yml
index de9ea0c32a..d574999e40 100644
--- a/locales/es-ES.yml
+++ b/locales/es-ES.yml
@@ -1915,7 +1915,6 @@ _theme:
     buttonBg: "Fondo de botón"
     buttonHoverBg: "Fondo de botón (hover)"
     inputBorder: "Borde de los campos de entrada"
-    listItemHoverBg: "Fondo de elemento de listas (hover)"
     driveFolderBg: "Fondo de capeta del drive"
     wallpaperOverlay: "Transparencia del fondo de pantalla"
     badge: "Medalla"
diff --git a/locales/fr-FR.yml b/locales/fr-FR.yml
index 7dfc64d63f..a7060c06fc 100644
--- a/locales/fr-FR.yml
+++ b/locales/fr-FR.yml
@@ -1701,7 +1701,6 @@ _theme:
     buttonBg: "Arrière-plan du bouton"
     buttonHoverBg: "Arrière-plan du bouton (survolé)"
     inputBorder: "Cadre de la zone de texte"
-    listItemHoverBg: "Arrière-plan d'item de liste (survolé)"
     driveFolderBg: "Arrière-plan du dossier de disque"
     wallpaperOverlay: "Superposition de fond d'écran"
     badge: "Badge"
diff --git a/locales/id-ID.yml b/locales/id-ID.yml
index fbfedb89e3..ce3958b167 100644
--- a/locales/id-ID.yml
+++ b/locales/id-ID.yml
@@ -1924,7 +1924,6 @@ _theme:
     buttonBg: "Latar belakang tombol"
     buttonHoverBg: "Latar belakang tombol (Mengambang)"
     inputBorder: "Batas bidang masukan"
-    listItemHoverBg: "Latar belakang daftar item (Mengambang)"
     driveFolderBg: "Latar belakang folder drive"
     wallpaperOverlay: "Lapisan wallpaper"
     badge: "Lencana"
diff --git a/locales/it-IT.yml b/locales/it-IT.yml
index 004bb6e9fd..d42fff326c 100644
--- a/locales/it-IT.yml
+++ b/locales/it-IT.yml
@@ -1975,7 +1975,6 @@ _theme:
     buttonBg: "Sfondo del pulsante"
     buttonHoverBg: "Sfondo del pulsante (sorvolato)"
     inputBorder: "Inquadra casella di testo"
-    listItemHoverBg: "Sfondo della voce di elenco (sorvolato)"
     driveFolderBg: "Sfondo della cartella di disco"
     wallpaperOverlay: "Sovrapposizione dello sfondo"
     badge: "Distintivo"
diff --git a/locales/ja-KS.yml b/locales/ja-KS.yml
index 52a8f41380..0a8b3828f2 100644
--- a/locales/ja-KS.yml
+++ b/locales/ja-KS.yml
@@ -1943,7 +1943,6 @@ _theme:
     buttonBg: "ボタンの背景"
     buttonHoverBg: "ボタンの背景 (ホバー)"
     inputBorder: "入力ボックスの縁取り"
-    listItemHoverBg: "リスト項目の背景 (ホバー)"
     driveFolderBg: "ドライブフォルダーの背景"
     wallpaperOverlay: "壁紙のオーバーレイ"
     badge: "バッジ"
diff --git a/locales/ko-KR.yml b/locales/ko-KR.yml
index 757afe53f9..973140dca2 100644
--- a/locales/ko-KR.yml
+++ b/locales/ko-KR.yml
@@ -1984,7 +1984,6 @@ _theme:
     buttonBg: "버튼 배경"
     buttonHoverBg: "버튼 배경 (호버)"
     inputBorder: "입력 필드 테두리"
-    listItemHoverBg: "리스트 항목 배경 (호버)"
     driveFolderBg: "드라이브 폴더 배경"
     wallpaperOverlay: "배경화면 오버레이"
     badge: "배지"
diff --git a/locales/pl-PL.yml b/locales/pl-PL.yml
index 117434ad32..d7afd57760 100644
--- a/locales/pl-PL.yml
+++ b/locales/pl-PL.yml
@@ -1205,7 +1205,6 @@ _theme:
     buttonBg: "Tło przycisku"
     buttonHoverBg: "Tło przycisku (po najechaniu)"
     inputBorder: "Obramowanie pola wejścia"
-    listItemHoverBg: "Tło elementu listy (po najechaniu)"
     driveFolderBg: "Tło folderu na dysku"
     wallpaperOverlay: "Nakładka tapety"
     badge: "Odznaka"
diff --git a/locales/pt-PT.yml b/locales/pt-PT.yml
index 98d42eb44a..9039fd2141 100644
--- a/locales/pt-PT.yml
+++ b/locales/pt-PT.yml
@@ -25,7 +25,7 @@ basicSettings: "Configurações básicas"
 otherSettings: "Outras configurações"
 openInWindow: "Abrir em um janela"
 profile: "Perfil"
-timeline: "Cronologia"
+timeline: "Linha do tempo"
 noAccountDescription: "Este usuário não tem uma descrição."
 login: "Iniciar sessão"
 loggingIn: "Iniciando sessão…"
@@ -1944,7 +1944,6 @@ _theme:
     buttonBg: "Plano de fundo de botão"
     buttonHoverBg: "Plano de fundo de botão (Selecionado)"
     inputBorder: "Borda de campo digitável"
-    listItemHoverBg: "Plano de fundo do item de uma lista (Selecionado)"
     driveFolderBg: "Plano de fundo da pasta no Drive"
     wallpaperOverlay: "Sobreposição do papel de parede."
     badge: "Emblema"
diff --git a/locales/ru-RU.yml b/locales/ru-RU.yml
index befb537105..70178ec2fd 100644
--- a/locales/ru-RU.yml
+++ b/locales/ru-RU.yml
@@ -1694,7 +1694,6 @@ _theme:
     buttonBg: "Фон кнопки"
     buttonHoverBg: "Текст кнопки"
     inputBorder: "Рамка поля ввода"
-    listItemHoverBg: "Фон пункта списка (под указателем)"
     driveFolderBg: "Фон папки «Диска»"
     wallpaperOverlay: "Слой обоев"
     badge: "Значок"
diff --git a/locales/sk-SK.yml b/locales/sk-SK.yml
index 8cb73e1303..60ce45a6b9 100644
--- a/locales/sk-SK.yml
+++ b/locales/sk-SK.yml
@@ -1108,7 +1108,6 @@ _theme:
     buttonBg: "Pozadie tlačidla"
     buttonHoverBg: "Pozadie tlačidla (pod kurzorom)"
     inputBorder: "Okraj vstupného poľa"
-    listItemHoverBg: "Pozadie položky zoznamu (pod kurzorom)"
     driveFolderBg: "Pozadie priečinu disku"
     wallpaperOverlay: "Vrstvenie pozadia"
     badge: "Odznak"
diff --git a/locales/th-TH.yml b/locales/th-TH.yml
index 31eee2bccc..c70d448e2b 100644
--- a/locales/th-TH.yml
+++ b/locales/th-TH.yml
@@ -1943,7 +1943,6 @@ _theme:
     buttonBg: "ปุ่มพื้นหลัง"
     buttonHoverBg: "ปุ่มพื้นหลัง (โฮเวอร์)"
     inputBorder: "เส้นขอบของช่องป้อนข้อมูล"
-    listItemHoverBg: "รายการไอเทมพื้นหลัง (โฮเวอร์)"
     driveFolderBg: "พื้นหลังโฟลเดอร์ไดรฟ์"
     wallpaperOverlay: "วอลล์เปเปอร์ซ้อนทับ"
     badge: "ตรา"
diff --git a/locales/uk-UA.yml b/locales/uk-UA.yml
index 974508b3a7..f2262cd71f 100644
--- a/locales/uk-UA.yml
+++ b/locales/uk-UA.yml
@@ -1302,7 +1302,6 @@ _theme:
     buttonBg: "Фон кнопки"
     buttonHoverBg: "Фон кнопки (при наведенні)"
     inputBorder: "Край поля вводу"
-    listItemHoverBg: "Фон елементу в списку (при наведенні)"
     driveFolderBg: "Фон папки на диску"
     wallpaperOverlay: "Накладання шпалер"
     badge: "Значок"
diff --git a/locales/vi-VN.yml b/locales/vi-VN.yml
index 6cf9b3f278..235497d844 100644
--- a/locales/vi-VN.yml
+++ b/locales/vi-VN.yml
@@ -1546,7 +1546,6 @@ _theme:
     buttonBg: "Nền nút"
     buttonHoverBg: "Nền nút (Chạm)"
     inputBorder: "Đường viền khung soạn thảo"
-    listItemHoverBg: "Nền mục liệt kê (Chạm)"
     driveFolderBg: "Nền thư mục Ổ đĩa"
     wallpaperOverlay: "Lớp phủ hình nền"
     badge: "Huy hiệu"
diff --git a/locales/zh-CN.yml b/locales/zh-CN.yml
index 09feca5b4e..8b681efb13 100644
--- a/locales/zh-CN.yml
+++ b/locales/zh-CN.yml
@@ -1984,7 +1984,6 @@ _theme:
     buttonBg: "按钮背景"
     buttonHoverBg: "按钮背景(悬停)"
     inputBorder: "输入框边框"
-    listItemHoverBg: "下拉列表项目背景(悬停)"
     driveFolderBg: "网盘的文件夹背景"
     wallpaperOverlay: "壁纸叠加层"
     badge: "徽章"
diff --git a/locales/zh-TW.yml b/locales/zh-TW.yml
index 5e8a5d8f8d..55b504e8fb 100644
--- a/locales/zh-TW.yml
+++ b/locales/zh-TW.yml
@@ -1975,7 +1975,6 @@ _theme:
     buttonBg: "按鈕背景"
     buttonHoverBg: "按鈕背景 (漂浮)"
     inputBorder: "輸入框邊框"
-    listItemHoverBg: "列表物品背景 (漂浮)"
     driveFolderBg: "雲端硬碟文件夾背景"
     wallpaperOverlay: "壁紙覆蓋層"
     badge: "徽章"

From ebae39cba542c91af8086adf126944d4eeaad188 Mon Sep 17 00:00:00 2001
From: "github-actions[bot]" <github-actions[bot]@users.noreply.github.com>
Date: Thu, 10 Oct 2024 05:17:00 +0000
Subject: [PATCH 073/121] Bump version to 2024.10.1-alpha.0

---
 CHANGELOG.md                     | 2 +-
 package.json                     | 2 +-
 packages/misskey-js/package.json | 2 +-
 3 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 36645aff74..0010b1fa5d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,4 +1,4 @@
-## Unreleased
+## 2024.10.1
 
 ### General
 -
diff --git a/package.json b/package.json
index 3afd84253a..743a2aefb9 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
 	"name": "misskey",
-	"version": "2024.10.0",
+	"version": "2024.10.1-alpha.0",
 	"codename": "nasubi",
 	"repository": {
 		"type": "git",
diff --git a/packages/misskey-js/package.json b/packages/misskey-js/package.json
index a7e04d6dac..251ce5a5fe 100644
--- a/packages/misskey-js/package.json
+++ b/packages/misskey-js/package.json
@@ -1,7 +1,7 @@
 {
 	"type": "module",
 	"name": "misskey-js",
-	"version": "2024.10.0",
+	"version": "2024.10.1-alpha.0",
 	"description": "Misskey SDK for JavaScript",
 	"license": "MIT",
 	"main": "./built/index.js",

From 21e51567e7cd7402f8c203845a593445507556aa Mon Sep 17 00:00:00 2001
From: "github-actions[bot]" <github-actions[bot]@users.noreply.github.com>
Date: Thu, 10 Oct 2024 05:56:04 +0000
Subject: [PATCH 074/121] Bump version to 2024.10.1-beta.1

---
 package.json                     | 2 +-
 packages/misskey-js/package.json | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/package.json b/package.json
index 743a2aefb9..7fc5d05a3f 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
 	"name": "misskey",
-	"version": "2024.10.1-alpha.0",
+	"version": "2024.10.1-beta.1",
 	"codename": "nasubi",
 	"repository": {
 		"type": "git",
diff --git a/packages/misskey-js/package.json b/packages/misskey-js/package.json
index 251ce5a5fe..0fea7a4d43 100644
--- a/packages/misskey-js/package.json
+++ b/packages/misskey-js/package.json
@@ -1,7 +1,7 @@
 {
 	"type": "module",
 	"name": "misskey-js",
-	"version": "2024.10.1-alpha.0",
+	"version": "2024.10.1-beta.1",
 	"description": "Misskey SDK for JavaScript",
 	"license": "MIT",
 	"main": "./built/index.js",

From b668d161a9a0a2f73c487f3fa6d54fd7597635a5 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=8B=E3=81=A3=E3=81=93=E3=81=8B=E3=82=8A?=
 <67428053+kakkokari-gtyih@users.noreply.github.com>
Date: Thu, 10 Oct 2024 16:12:16 +0900
Subject: [PATCH 075/121] refactor(frontend): prefix css variables (UI)
 (#14739)

* refactor(frontend): prefix css variables

* `MI_UI` -> `MI`

* fix

* `stickyBottom`

* stickyTop
---
 .../src/components/EmMediaBanner.vue          |  4 +--
 .../src/components/EmMediaVideo.vue           |  4 +--
 .../frontend-embed/src/components/EmNote.vue  | 10 +++---
 .../src/components/EmNoteDetailed.vue         |  2 +-
 .../src/components/EmNoteSimple.vue           |  2 +-
 .../src/components/EmSubNoteContent.vue       |  2 +-
 packages/frontend-embed/src/pages/clip.vue    |  2 +-
 packages/frontend-embed/src/pages/tag.vue     |  2 +-
 .../src/pages/user-timeline.vue               |  2 +-
 packages/frontend-embed/src/style.scss        | 22 ++++++------
 packages/frontend-embed/src/ui.vue            |  2 +-
 packages/frontend/src/boot/common.ts          |  6 ++--
 .../src/components/MkAccountMoved.vue         |  2 +-
 .../src/components/MkAnnouncementDialog.vue   |  2 +-
 packages/frontend/src/components/MkChart.vue  |  4 +--
 .../frontend/src/components/MkContainer.vue   |  4 +--
 .../src/components/MkCropperDialog.vue        |  4 +--
 .../MkCustomEmojiDetailedDialog.vue           |  4 +--
 .../src/components/MkDateSeparatedList.vue    |  2 +-
 .../frontend/src/components/MkDonation.vue    |  4 +--
 packages/frontend/src/components/MkDrive.vue  |  2 +-
 .../src/components/MkExtensionInstaller.vue   |  2 +-
 .../src/components/MkFoldableSection.vue      |  6 ++--
 packages/frontend/src/components/MkFolder.vue | 10 +++---
 .../frontend/src/components/MkFukidashi.vue   |  2 +-
 packages/frontend/src/components/MkInfo.vue   |  2 +-
 .../src/components/MkInstanceStats.vue        |  2 +-
 .../frontend/src/components/MkMediaAudio.vue  |  4 +--
 .../frontend/src/components/MkMediaImage.vue  |  4 +--
 .../frontend/src/components/MkMediaList.vue   |  6 ++--
 .../frontend/src/components/MkMediaVideo.vue  |  2 +-
 .../frontend/src/components/MkModalWindow.vue |  6 ++--
 packages/frontend/src/components/MkNote.vue   | 10 +++---
 .../src/components/MkNoteDetailed.vue         |  4 +--
 .../frontend/src/components/MkNoteSimple.vue  |  2 +-
 packages/frontend/src/components/MkNotes.vue  |  2 +-
 packages/frontend/src/components/MkOmit.vue   |  2 +-
 .../frontend/src/components/MkPagePreview.vue |  8 ++---
 .../frontend/src/components/MkPageWindow.vue  |  2 +-
 .../src/components/MkRemoteCaution.vue        |  2 +-
 .../src/components/MkSigninDialog.vue         |  4 +--
 .../src/components/MkSignupDialog.rules.vue   |  2 +-
 .../components/MkSourceCodeAvailablePopup.vue |  4 +--
 .../src/components/MkSubNoteContent.vue       |  2 +-
 .../src/components/MkSystemWebhookEditor.vue  |  4 +--
 .../frontend/src/components/MkTextarea.vue    |  2 +-
 .../src/components/MkTokenGenerateWindow.vue  |  2 +-
 .../src/components/MkTutorialDialog.Note.vue  |  2 +-
 .../components/MkTutorialDialog.PostNote.vue  |  2 +-
 .../components/MkTutorialDialog.Sensitive.vue |  2 +-
 .../components/MkTutorialDialog.Timeline.vue  |  2 +-
 .../frontend/src/components/MkUpdated.vue     |  2 +-
 .../MkUserAnnouncementEditDialog.vue          |  4 +--
 .../frontend/src/components/MkUserList.vue    |  2 +-
 .../components/MkUserSetupDialog.Follow.vue   |  2 +-
 .../src/components/MkVisitorDashboard.vue     |  2 +-
 .../src/components/MkWaitingDialog.vue        |  2 +-
 .../frontend/src/components/MkWidgets.vue     |  4 +--
 packages/frontend/src/components/MkWindow.vue |  6 ++--
 .../src/components/global/MkPageHeader.vue    |  6 ++--
 .../components/global/MkStickyContainer.vue   |  8 ++---
 .../src/components/page/page.dynamic.vue      |  4 +--
 .../src/components/page/page.image.vue        |  2 +-
 .../src/components/page/page.note.vue         |  2 +-
 packages/frontend/src/pages/about-misskey.vue |  2 +-
 .../frontend/src/pages/about.federation.vue   |  2 +-
 .../frontend/src/pages/about.overview.vue     |  2 +-
 .../src/pages/admin/RolesEditorFormula.vue    |  2 +-
 .../frontend/src/pages/admin/_header_.vue     |  4 +--
 .../notification-recipient.editor.vue         |  4 +--
 packages/frontend/src/pages/admin/ads.vue     |  2 +-
 .../frontend/src/pages/admin/branding.vue     |  4 +--
 .../src/pages/admin/email-settings.vue        |  4 +--
 .../frontend/src/pages/admin/federation.vue   |  2 +-
 packages/frontend/src/pages/admin/files.vue   |  4 +--
 .../src/pages/admin/modlog.ModLog.vue         |  2 +-
 packages/frontend/src/pages/admin/modlog.vue  |  6 ++--
 .../src/pages/admin/object-storage.vue        |  4 +--
 .../src/pages/admin/overview.queue.vue        |  2 +-
 .../frontend/src/pages/admin/queue.chart.vue  |  2 +-
 .../frontend/src/pages/admin/roles.edit.vue   |  4 +--
 .../frontend/src/pages/antenna-timeline.vue   |  8 ++---
 .../frontend/src/pages/avatar-decorations.vue |  8 ++---
 packages/frontend/src/pages/channel.vue       |  6 ++--
 .../src/pages/custom-emojis-manager.vue       |  8 ++---
 .../frontend/src/pages/drive.file.info.vue    |  6 ++--
 .../frontend/src/pages/emoji-edit-dialog.vue  |  4 +--
 .../frontend/src/pages/explore.featured.vue   |  2 +-
 packages/frontend/src/pages/explore.users.vue |  2 +-
 packages/frontend/src/pages/favorites.vue     |  2 +-
 .../frontend/src/pages/flash/flash-edit.vue   |  2 +-
 packages/frontend/src/pages/gallery/index.vue |  2 +-
 packages/frontend/src/pages/gallery/post.vue  |  2 +-
 .../frontend/src/pages/install-extensions.vue |  2 +-
 packages/frontend/src/pages/list.vue          |  2 +-
 packages/frontend/src/pages/my-lists/list.vue |  6 ++--
 packages/frontend/src/pages/note.vue          |  6 ++--
 packages/frontend/src/pages/notifications.vue |  2 +-
 packages/frontend/src/pages/page.vue          | 12 +++----
 .../src/pages/reversi/game.setting.vue        |  4 +--
 packages/frontend/src/pages/reversi/index.vue |  2 +-
 packages/frontend/src/pages/scratchpad.vue    |  2 +-
 .../settings/avatar-decoration.dialog.vue     |  4 +--
 .../src/pages/settings/avatar-decoration.vue  |  2 +-
 .../src/pages/settings/emoji-picker.vue       |  4 +--
 .../pages/settings/preferences-backups.vue    |  2 +-
 .../frontend/src/pages/settings/theme.vue     |  2 +-
 .../frontend/src/pages/signup-complete.vue    |  2 +-
 packages/frontend/src/pages/tag.vue           |  4 +--
 packages/frontend/src/pages/timeline.vue      | 14 ++++----
 .../frontend/src/pages/user-list-timeline.vue |  8 ++---
 .../frontend/src/pages/user/follow-list.vue   |  2 +-
 packages/frontend/src/pages/user/gallery.vue  |  2 +-
 packages/frontend/src/pages/user/home.vue     | 14 ++++----
 .../src/pages/user/index.timeline.vue         |  4 +--
 .../frontend/src/pages/welcome.entrance.a.vue |  4 +--
 packages/frontend/src/pages/welcome.setup.vue |  2 +-
 .../frontend/src/pages/welcome.timeline.vue   |  4 +--
 packages/frontend/src/style.scss              | 34 +++++++++----------
 packages/frontend/src/ui/_common_/common.vue  | 10 +++---
 .../src/ui/_common_/navbar-for-mobile.vue     |  8 ++---
 packages/frontend/src/ui/_common_/navbar.vue  | 16 ++++-----
 .../src/ui/_common_/stream-indicator.vue      |  4 +--
 packages/frontend/src/ui/classic.vue          | 18 +++++-----
 packages/frontend/src/ui/deck.vue             |  6 ++--
 packages/frontend/src/ui/deck/column.vue      |  4 +--
 .../frontend/src/ui/deck/widgets-column.vue   |  4 +--
 packages/frontend/src/ui/universal.vue        | 18 +++++-----
 packages/frontend/src/ui/zen.vue              |  8 ++---
 .../src/widgets/WidgetBirthdayFollowings.vue  |  6 ++--
 130 files changed, 296 insertions(+), 296 deletions(-)

diff --git a/packages/frontend-embed/src/components/EmMediaBanner.vue b/packages/frontend-embed/src/components/EmMediaBanner.vue
index 3e3dfd95b2..cf4a4c53b5 100644
--- a/packages/frontend-embed/src/components/EmMediaBanner.vue
+++ b/packages/frontend-embed/src/components/EmMediaBanner.vue
@@ -31,10 +31,10 @@ defineProps<{
 	display: flex;
 	align-items: center;
 	width: 100%;
-	padding: var(--margin);
+	padding: var(--MI-margin);
 	margin-top: 4px;
 	border: 1px solid var(--MI_THEME-inputBorder);
-	border-radius: var(--radius);
+	border-radius: var(--MI-radius);
 	background-color: var(--MI_THEME-panel);
 	transition: background-color .1s, border-color .1s;
 
diff --git a/packages/frontend-embed/src/components/EmMediaVideo.vue b/packages/frontend-embed/src/components/EmMediaVideo.vue
index 5ca0b92d43..e2779bdee4 100644
--- a/packages/frontend-embed/src/components/EmMediaVideo.vue
+++ b/packages/frontend-embed/src/components/EmMediaVideo.vue
@@ -29,9 +29,9 @@ defineProps<{
 	width: 100%;
 	height: auto;
 	aspect-ratio: 16 / 9;
-	padding: var(--margin);
+	padding: var(--MI-margin);
 	border: 1px solid var(--MI_THEME-divider);
-	border-radius: var(--radius);
+	border-radius: var(--MI-radius);
 	background-color: #000;
 
 	&:hover {
diff --git a/packages/frontend-embed/src/components/EmNote.vue b/packages/frontend-embed/src/components/EmNote.vue
index 7eeeda1797..d4b4827c90 100644
--- a/packages/frontend-embed/src/components/EmNote.vue
+++ b/packages/frontend-embed/src/components/EmNote.vue
@@ -190,7 +190,7 @@ const isDeleted = ref(false);
 			width: calc(100% - 8px);
 			height: calc(100% - 8px);
 			border: dashed 2px var(--MI_THEME-focus);
-			border-radius: var(--radius);
+			border-radius: var(--MI-radius);
 			box-sizing: border-box;
 		}
 	}
@@ -356,7 +356,7 @@ const isDeleted = ref(false);
 	width: 58px;
 	height: 58px;
 	position: sticky !important;
-	top: calc(22px + var(--stickyTop, 0px));
+	top: calc(22px + var(--MI-stickyTop, 0px));
 	left: 0;
 }
 
@@ -377,7 +377,7 @@ const isDeleted = ref(false);
 	width: 100%;
 	margin-top: 14px;
 	position: sticky;
-	bottom: calc(var(--stickyBottom, 0px) + 14px);
+	bottom: calc(var(--MI-stickyBottom, 0px) + 14px);
 }
 
 .showLessLabel {
@@ -430,7 +430,7 @@ const isDeleted = ref(false);
 
 .translation {
 	border: solid 0.5px var(--MI_THEME-divider);
-	border-radius: var(--radius);
+	border-radius: var(--MI-radius);
 	padding: 12px;
 	margin-top: 8px;
 }
@@ -550,7 +550,7 @@ const isDeleted = ref(false);
 		margin: 0 10px 0 0;
 		width: 46px;
 		height: 46px;
-		top: calc(14px + var(--stickyTop, 0px));
+		top: calc(14px + var(--MI-stickyTop, 0px));
 	}
 }
 
diff --git a/packages/frontend-embed/src/components/EmNoteDetailed.vue b/packages/frontend-embed/src/components/EmNoteDetailed.vue
index ccd723d7d2..b39b47c065 100644
--- a/packages/frontend-embed/src/components/EmNoteDetailed.vue
+++ b/packages/frontend-embed/src/components/EmNoteDetailed.vue
@@ -364,7 +364,7 @@ const collapsed = ref(appearNote.value.cw == null && isLong);
 	width: 100%;
 	margin-top: 14px;
 	position: sticky;
-	bottom: calc(var(--stickyBottom, 0px) + 14px);
+	bottom: calc(var(--MI-stickyBottom, 0px) + 14px);
 }
 
 .showLessLabel {
diff --git a/packages/frontend-embed/src/components/EmNoteSimple.vue b/packages/frontend-embed/src/components/EmNoteSimple.vue
index 704a876e59..b9aaf3fa4a 100644
--- a/packages/frontend-embed/src/components/EmNoteSimple.vue
+++ b/packages/frontend-embed/src/components/EmNoteSimple.vue
@@ -53,7 +53,7 @@ const showContent = ref(false);
 	height: 34px;
 	border-radius: 8px;
 	position: sticky !important;
-	top: calc(16px + var(--stickyTop, 0px));
+	top: calc(16px + var(--MI-stickyTop, 0px));
 	left: 0;
 }
 
diff --git a/packages/frontend-embed/src/components/EmSubNoteContent.vue b/packages/frontend-embed/src/components/EmSubNoteContent.vue
index dcaa1ec914..61815ddfd8 100644
--- a/packages/frontend-embed/src/components/EmSubNoteContent.vue
+++ b/packages/frontend-embed/src/components/EmSubNoteContent.vue
@@ -100,7 +100,7 @@ const collapsed = ref(isLong);
 	width: 100%;
 	margin-top: 14px;
 	position: sticky;
-	bottom: calc(var(--stickyBottom, 0px) + 14px);
+	bottom: calc(var(--MI-stickyBottom, 0px) + 14px);
 }
 
 .showLessLabel {
diff --git a/packages/frontend-embed/src/pages/clip.vue b/packages/frontend-embed/src/pages/clip.vue
index d805cb3e4f..f4d4e8cf6f 100644
--- a/packages/frontend-embed/src/pages/clip.vue
+++ b/packages/frontend-embed/src/pages/clip.vue
@@ -100,7 +100,7 @@ function top(ev: MouseEvent) {
 	display: flex;
 	min-width: 0;
 	align-items: center;
-	gap: var(--margin);
+	gap: var(--MI-margin);
 	overflow: hidden;
 
 	.headerClipIconRoot {
diff --git a/packages/frontend-embed/src/pages/tag.vue b/packages/frontend-embed/src/pages/tag.vue
index 78049e4041..4b00ae7c2d 100644
--- a/packages/frontend-embed/src/pages/tag.vue
+++ b/packages/frontend-embed/src/pages/tag.vue
@@ -83,7 +83,7 @@ function top(ev: MouseEvent) {
 	display: flex;
 	min-width: 0;
 	align-items: center;
-	gap: var(--margin);
+	gap: var(--MI-margin);
 	overflow: hidden;
 
 	.headerClipIconRoot {
diff --git a/packages/frontend-embed/src/pages/user-timeline.vue b/packages/frontend-embed/src/pages/user-timeline.vue
index 85e6f52d50..348b1a7622 100644
--- a/packages/frontend-embed/src/pages/user-timeline.vue
+++ b/packages/frontend-embed/src/pages/user-timeline.vue
@@ -117,7 +117,7 @@ function top(ev: MouseEvent) {
 	display: flex;
 	min-width: 0;
 	align-items: center;
-	gap: var(--margin);
+	gap: var(--MI-margin);
 	overflow: hidden;
 
 	.avatarLink {
diff --git a/packages/frontend-embed/src/style.scss b/packages/frontend-embed/src/style.scss
index 1569de01f8..2e43cfd20a 100644
--- a/packages/frontend-embed/src/style.scss
+++ b/packages/frontend-embed/src/style.scss
@@ -7,11 +7,11 @@
  */
 
 :root {
-	--radius: 12px;
-	--marginFull: 14px;
-	--marginHalf: 10px;
+	--MI-radius: 12px;
+	--MI-marginFull: 14px;
+	--MI-marginHalf: 10px;
 
-	--margin: var(--marginFull);
+	--MI-margin: var(--MI-marginFull);
 }
 
 html {
@@ -218,12 +218,12 @@ rt {
 
 ._panel {
 	background: var(--MI_THEME-panel);
-	border-radius: var(--radius);
+	border-radius: var(--MI-radius);
 	overflow: clip;
 }
 
 ._margin {
-	margin: var(--margin) 0;
+	margin: var(--MI-margin) 0;
 }
 
 ._gaps_m {
@@ -241,7 +241,7 @@ rt {
 ._gaps {
 	display: flex;
 	flex-direction: column;
-	gap: var(--margin);
+	gap: var(--MI-margin);
 }
 
 ._buttons {
@@ -264,7 +264,7 @@ rt {
 	box-sizing: border-box;
 	text-align: center;
 	border: solid 0.5px var(--MI_THEME-divider);
-	border-radius: var(--radius);
+	border-radius: var(--MI-radius);
 
 	&:active {
 		border-color: var(--MI_THEME-accent);
@@ -273,14 +273,14 @@ rt {
 
 ._popup {
 	background: var(--MI_THEME-popup);
-	border-radius: var(--radius);
+	border-radius: var(--MI-radius);
 	contain: content;
 }
 
 ._acrylic {
 	background: var(--MI_THEME-acrylicPanel);
-	-webkit-backdrop-filter: var(--blur, blur(15px));
-	backdrop-filter: var(--blur, blur(15px));
+	-webkit-backdrop-filter: var(--MI-blur, blur(15px));
+	backdrop-filter: var(--MI-blur, blur(15px));
 }
 
 ._fullinfo {
diff --git a/packages/frontend-embed/src/ui.vue b/packages/frontend-embed/src/ui.vue
index 2ed2f58376..4ba5968a91 100644
--- a/packages/frontend-embed/src/ui.vue
+++ b/packages/frontend-embed/src/ui.vue
@@ -95,7 +95,7 @@ onUnmounted(() => {
 	height: auto;
 
 	&.rounded {
-		border-radius: var(--radius);
+		border-radius: var(--MI-radius);
 	}
 
 	&.noBorder {
diff --git a/packages/frontend/src/boot/common.ts b/packages/frontend/src/boot/common.ts
index 52f8fb49e5..1145891b71 100644
--- a/packages/frontend/src/boot/common.ts
+++ b/packages/frontend/src/boot/common.ts
@@ -186,14 +186,14 @@ export async function common(createVue: () => App<Element>) {
 	});
 
 	watch(defaultStore.reactiveState.useBlurEffectForModal, v => {
-		document.documentElement.style.setProperty('--modalBgFilter', v ? 'blur(4px)' : 'none');
+		document.documentElement.style.setProperty('--MI-modalBgFilter', v ? 'blur(4px)' : 'none');
 	}, { immediate: true });
 
 	watch(defaultStore.reactiveState.useBlurEffect, v => {
 		if (v) {
-			document.documentElement.style.removeProperty('--blur');
+			document.documentElement.style.removeProperty('--MI-blur');
 		} else {
-			document.documentElement.style.setProperty('--blur', 'none');
+			document.documentElement.style.setProperty('--MI-blur', 'none');
 		}
 	}, { immediate: true });
 
diff --git a/packages/frontend/src/components/MkAccountMoved.vue b/packages/frontend/src/components/MkAccountMoved.vue
index bd6f8ceb09..0839955d9d 100644
--- a/packages/frontend/src/components/MkAccountMoved.vue
+++ b/packages/frontend/src/components/MkAccountMoved.vue
@@ -34,7 +34,7 @@ misskeyApi('users/show', { userId: props.movedTo }).then(u => user.value = u);
 	font-size: 90%;
 	background: var(--MI_THEME-infoWarnBg);
 	color: var(--MI_THEME-error);
-	border-radius: var(--radius);
+	border-radius: var(--MI-radius);
 }
 
 .link {
diff --git a/packages/frontend/src/components/MkAnnouncementDialog.vue b/packages/frontend/src/components/MkAnnouncementDialog.vue
index 488492701e..1adb244c9e 100644
--- a/packages/frontend/src/components/MkAnnouncementDialog.vue
+++ b/packages/frontend/src/components/MkAnnouncementDialog.vue
@@ -84,7 +84,7 @@ onMounted(() => {
 	max-width: 480px;
 	box-sizing: border-box;
 	background: var(--MI_THEME-panel);
-	border-radius: var(--radius);
+	border-radius: var(--MI-radius);
 }
 
 .header {
diff --git a/packages/frontend/src/components/MkChart.vue b/packages/frontend/src/components/MkChart.vue
index 57d325b11a..d05f4921f6 100644
--- a/packages/frontend/src/components/MkChart.vue
+++ b/packages/frontend/src/components/MkChart.vue
@@ -863,8 +863,8 @@ onMounted(() => {
 	left: 0;
 	width: 100%;
 	height: 100%;
-	-webkit-backdrop-filter: var(--blur, blur(12px));
-	backdrop-filter: var(--blur, blur(12px));
+	-webkit-backdrop-filter: var(--MI-blur, blur(12px));
+	backdrop-filter: var(--MI-blur, blur(12px));
 	display: flex;
 	justify-content: center;
 	align-items: center;
diff --git a/packages/frontend/src/components/MkContainer.vue b/packages/frontend/src/components/MkContainer.vue
index f2bafb4adf..8ab01d7db8 100644
--- a/packages/frontend/src/components/MkContainer.vue
+++ b/packages/frontend/src/components/MkContainer.vue
@@ -165,7 +165,7 @@ onUnmounted(() => {
 
 .header {
 	position: sticky;
-	top: var(--stickyTop, 0px);
+	top: var(--MI-stickyTop, 0px);
 	left: 0;
 	color: var(--MI_THEME-panelHeaderFg);
 	background: var(--MI_THEME-panelHeaderBg);
@@ -201,7 +201,7 @@ onUnmounted(() => {
 }
 
 .content {
-	--stickyTop: 0px;
+	--MI-stickyTop: 0px;
 
 	&.omitted {
 		position: relative;
diff --git a/packages/frontend/src/components/MkCropperDialog.vue b/packages/frontend/src/components/MkCropperDialog.vue
index a25dc36882..c2a1aaf29a 100644
--- a/packages/frontend/src/components/MkCropperDialog.vue
+++ b/packages/frontend/src/components/MkCropperDialog.vue
@@ -170,8 +170,8 @@ onMounted(() => {
 		display: flex;
 		align-items: center;
 		justify-content: center;
-		-webkit-backdrop-filter: var(--blur, blur(10px));
-		backdrop-filter: var(--blur, blur(10px));
+		-webkit-backdrop-filter: var(--MI-blur, blur(10px));
+		backdrop-filter: var(--MI-blur, blur(10px));
 		background: rgba(0, 0, 0, 0.5);
 	}
 
diff --git a/packages/frontend/src/components/MkCustomEmojiDetailedDialog.vue b/packages/frontend/src/components/MkCustomEmojiDetailedDialog.vue
index 29a435fb1a..949adc6a8e 100644
--- a/packages/frontend/src/components/MkCustomEmojiDetailedDialog.vue
+++ b/packages/frontend/src/components/MkCustomEmojiDetailedDialog.vue
@@ -86,7 +86,7 @@ function cancel() {
   max-width: 100%;
   height: 40cqh;
   background-image: repeating-linear-gradient(45deg, transparent, transparent 8px, var(--MI_THEME-X5) 8px, var(--MI_THEME-X5) 14px);
-  border-radius: var(--radius);
+  border-radius: var(--MI-radius);
   margin: auto;
   overflow-y: hidden;
 }
@@ -103,6 +103,6 @@ function cancel() {
   padding: 3px 10px;
   background-color: var(--MI_THEME-X5);
   border: solid 1px var(--MI_THEME-divider);
-  border-radius: var(--radius);
+  border-radius: var(--MI-radius);
 }
 </style>
diff --git a/packages/frontend/src/components/MkDateSeparatedList.vue b/packages/frontend/src/components/MkDateSeparatedList.vue
index 0886b7a4f7..a8a32e8bc7 100644
--- a/packages/frontend/src/components/MkDateSeparatedList.vue
+++ b/packages/frontend/src/components/MkDateSeparatedList.vue
@@ -182,7 +182,7 @@ export default defineComponent({
 	}
 
 	&:not(.date-separated-list-nogap) > *:not(:last-child) {
-		margin-bottom: var(--margin);
+		margin-bottom: var(--MI-margin);
 	}
 }
 
diff --git a/packages/frontend/src/components/MkDonation.vue b/packages/frontend/src/components/MkDonation.vue
index ebface5185..0e0da64750 100644
--- a/packages/frontend/src/components/MkDonation.vue
+++ b/packages/frontend/src/components/MkDonation.vue
@@ -65,12 +65,12 @@ function neverShow() {
 .root {
 	position: fixed;
 	z-index: v-bind(zIndex);
-	bottom: var(--margin);
+	bottom: var(--MI-margin);
 	left: 0;
 	right: 0;
 	margin: auto;
 	box-sizing: border-box;
-	width: calc(100% - (var(--margin) * 2));
+	width: calc(100% - (var(--MI-margin) * 2));
 	max-width: 500px;
 	display: flex;
 }
diff --git a/packages/frontend/src/components/MkDrive.vue b/packages/frontend/src/components/MkDrive.vue
index 8bd7ee8324..23883a44e9 100644
--- a/packages/frontend/src/components/MkDrive.vue
+++ b/packages/frontend/src/components/MkDrive.vue
@@ -768,7 +768,7 @@ onBeforeUnmount(() => {
 .main {
 	flex: 1;
 	overflow: auto;
-	padding: var(--margin);
+	padding: var(--MI-margin);
 	user-select: none;
 
 	&.fetching {
diff --git a/packages/frontend/src/components/MkExtensionInstaller.vue b/packages/frontend/src/components/MkExtensionInstaller.vue
index ed29dade7a..b41604b2c3 100644
--- a/packages/frontend/src/components/MkExtensionInstaller.vue
+++ b/packages/frontend/src/components/MkExtensionInstaller.vue
@@ -110,7 +110,7 @@ const emits = defineEmits<{
 
 <style lang="scss" module>
 .extInstallerRoot {
-	border-radius: var(--radius);
+	border-radius: var(--MI-radius);
 	background: var(--MI_THEME-panel);
 	padding: 1.5rem;
 }
diff --git a/packages/frontend/src/components/MkFoldableSection.vue b/packages/frontend/src/components/MkFoldableSection.vue
index ef1d075360..1717f8fc98 100644
--- a/packages/frontend/src/components/MkFoldableSection.vue
+++ b/packages/frontend/src/components/MkFoldableSection.vue
@@ -118,9 +118,9 @@ onMounted(() => {
 	position: relative;
 	z-index: 10;
 	position: sticky;
-	top: var(--stickyTop, 0px);
-	-webkit-backdrop-filter: var(--blur, blur(8px));
-	backdrop-filter: var(--blur, blur(20px));
+	top: var(--MI-stickyTop, 0px);
+	-webkit-backdrop-filter: var(--MI-blur, blur(8px));
+	backdrop-filter: var(--MI-blur, blur(20px));
 }
 
 .title {
diff --git a/packages/frontend/src/components/MkFolder.vue b/packages/frontend/src/components/MkFolder.vue
index 290d73dd92..5f9500d923 100644
--- a/packages/frontend/src/components/MkFolder.vue
+++ b/packages/frontend/src/components/MkFolder.vue
@@ -145,8 +145,8 @@ onMounted(() => {
 	box-sizing: border-box;
 	padding: 9px 12px 9px 12px;
 	background: var(--MI_THEME-folderHeaderBg);
-	-webkit-backdrop-filter: var(--blur, blur(15px));
-	backdrop-filter: var(--blur, blur(15px));
+	-webkit-backdrop-filter: var(--MI-blur, blur(15px));
+	backdrop-filter: var(--MI-blur, blur(15px));
 	border-radius: 6px;
 	transition: border-radius 0.3s;
 
@@ -236,12 +236,12 @@ onMounted(() => {
 .footer {
 	position: sticky !important;
 	z-index: 1;
-	bottom: var(--stickyBottom, 0px);
+	bottom: var(--MI-stickyBottom, 0px);
 	left: 0;
 	padding: 12px;
 	background: var(--MI_THEME-acrylicBg);
-	-webkit-backdrop-filter: var(--blur, blur(15px));
-	backdrop-filter: var(--blur, blur(15px));
+	-webkit-backdrop-filter: var(--MI-blur, blur(15px));
+	backdrop-filter: var(--MI-blur, blur(15px));
 	background-size: auto auto;
 	background-image: repeating-linear-gradient(135deg, transparent, transparent 5px, var(--MI_THEME-panel) 5px, var(--MI_THEME-panel) 10px);
 	border-radius: 0 0 6px 6px;
diff --git a/packages/frontend/src/components/MkFukidashi.vue b/packages/frontend/src/components/MkFukidashi.vue
index 307cd15dc8..8b1c56fca4 100644
--- a/packages/frontend/src/components/MkFukidashi.vue
+++ b/packages/frontend/src/components/MkFukidashi.vue
@@ -39,7 +39,7 @@ withDefaults(defineProps<{
 
 <style module lang="scss">
 .root {
-	--fukidashi-radius: var(--radius);
+	--fukidashi-radius: var(--MI-radius);
 	--fukidashi-bg: var(--MI_THEME-panel);
 
 	position: relative;
diff --git a/packages/frontend/src/components/MkInfo.vue b/packages/frontend/src/components/MkInfo.vue
index 87c98cf072..90ca1b5a9d 100644
--- a/packages/frontend/src/components/MkInfo.vue
+++ b/packages/frontend/src/components/MkInfo.vue
@@ -38,7 +38,7 @@ function close() {
 	font-size: 90%;
 	background: var(--MI_THEME-infoBg);
 	color: var(--MI_THEME-infoFg);
-	border-radius: var(--radius);
+	border-radius: var(--MI-radius);
 	white-space: pre-wrap;
 
 	&.warn {
diff --git a/packages/frontend/src/components/MkInstanceStats.vue b/packages/frontend/src/components/MkInstanceStats.vue
index da313d4d70..8ccbf61e48 100644
--- a/packages/frontend/src/components/MkInstanceStats.vue
+++ b/packages/frontend/src/components/MkInstanceStats.vue
@@ -257,7 +257,7 @@ onMounted(() => {
 				min-width: 0;
 				position: relative;
 				background: var(--MI_THEME-panel);
-				border-radius: var(--radius);
+				border-radius: var(--MI-radius);
 				padding: 24px;
 				max-height: 300px;
 
diff --git a/packages/frontend/src/components/MkMediaAudio.vue b/packages/frontend/src/components/MkMediaAudio.vue
index 915d67db7f..8b713b2734 100644
--- a/packages/frontend/src/components/MkMediaAudio.vue
+++ b/packages/frontend/src/components/MkMediaAudio.vue
@@ -392,7 +392,7 @@ onDeactivated(() => {
 	container-type: inline-size;
 	position: relative;
 	border: .5px solid var(--MI_THEME-divider);
-	border-radius: var(--radius);
+	border-radius: var(--MI-radius);
 	overflow: clip;
 
 	&:focus-visible {
@@ -454,7 +454,7 @@ onDeactivated(() => {
 
 	.controlButton {
 		padding: 6px;
-		border-radius: calc(var(--radius) / 2);
+		border-radius: calc(var(--MI-radius) / 2);
 		font-size: 1.05rem;
 
 		&:hover {
diff --git a/packages/frontend/src/components/MkMediaImage.vue b/packages/frontend/src/components/MkMediaImage.vue
index fbd973c196..ec85569df5 100644
--- a/packages/frontend/src/components/MkMediaImage.vue
+++ b/packages/frontend/src/components/MkMediaImage.vue
@@ -226,8 +226,8 @@ html[data-color-scheme=light] .visible {
 	position: absolute;
 	border-radius: 999px;
 	background-color: rgba(0, 0, 0, 0.3);
-	-webkit-backdrop-filter: var(--blur, blur(15px));
-	backdrop-filter: var(--blur, blur(15px));
+	-webkit-backdrop-filter: var(--MI-blur, blur(15px));
+	backdrop-filter: var(--MI-blur, blur(15px));
 	color: #fff;
 	font-size: 0.8em;
 	width: 28px;
diff --git a/packages/frontend/src/components/MkMediaList.vue b/packages/frontend/src/components/MkMediaList.vue
index 9fab73d87b..32766f2029 100644
--- a/packages/frontend/src/components/MkMediaList.vue
+++ b/packages/frontend/src/components/MkMediaList.vue
@@ -317,7 +317,7 @@ defineExpose({
 <style lang="scss">
 .pswp__bg {
 	background: var(--MI_THEME-modalBg);
-	backdrop-filter: var(--modalBgFilter);
+	backdrop-filter: var(--MI-modalBgFilter);
 }
 
 .pswp__alt-text-container {
@@ -338,8 +338,8 @@ defineExpose({
 	color: var(--MI_THEME-fg);
 	margin: 0 auto;
 	text-align: center;
-	padding: var(--margin);
-	border-radius: var(--radius);
+	padding: var(--MI-margin);
+	border-radius: var(--MI-radius);
 	max-height: 8em;
 	overflow-y: auto;
 	text-shadow: var(--MI_THEME-bg) 0 0 10px, var(--MI_THEME-bg) 0 0 3px, var(--MI_THEME-bg) 0 0 3px;
diff --git a/packages/frontend/src/components/MkMediaVideo.vue b/packages/frontend/src/components/MkMediaVideo.vue
index 271c66552b..d3a12ca734 100644
--- a/packages/frontend/src/components/MkMediaVideo.vue
+++ b/packages/frontend/src/components/MkMediaVideo.vue
@@ -658,7 +658,7 @@ onDeactivated(() => {
 
 	.controlButton {
 		padding: 6px;
-		border-radius: calc(var(--radius) / 2);
+		border-radius: calc(var(--MI-radius) / 2);
 		transition: background-color .2s ease-in-out;
 		font-size: 1.05rem;
 
diff --git a/packages/frontend/src/components/MkModalWindow.vue b/packages/frontend/src/components/MkModalWindow.vue
index c77611ef12..fe9e1ce088 100644
--- a/packages/frontend/src/components/MkModalWindow.vue
+++ b/packages/frontend/src/components/MkModalWindow.vue
@@ -90,7 +90,7 @@ defineExpose({
 	display: flex;
 	flex-direction: column;
 	contain: content;
-	border-radius: var(--radius);
+	border-radius: var(--MI-radius);
 
 	--root-margin: 24px;
 
@@ -106,8 +106,8 @@ defineExpose({
 	display: flex;
 	flex-shrink: 0;
 	background: var(--MI_THEME-windowHeader);
-	-webkit-backdrop-filter: var(--blur, blur(15px));
-	backdrop-filter: var(--blur, blur(15px));
+	-webkit-backdrop-filter: var(--MI-blur, blur(15px));
+	backdrop-filter: var(--MI-blur, blur(15px));
 }
 
 .headerButton {
diff --git a/packages/frontend/src/components/MkNote.vue b/packages/frontend/src/components/MkNote.vue
index c5f5431dcf..be93b3c529 100644
--- a/packages/frontend/src/components/MkNote.vue
+++ b/packages/frontend/src/components/MkNote.vue
@@ -644,7 +644,7 @@ function emitUpdReaction(emoji: string, delta: number) {
 			width: calc(100% - 8px);
 			height: calc(100% - 8px);
 			border: dashed 2px var(--MI_THEME-focus);
-			border-radius: var(--radius);
+			border-radius: var(--MI-radius);
 			box-sizing: border-box;
 		}
 	}
@@ -810,7 +810,7 @@ function emitUpdReaction(emoji: string, delta: number) {
 	width: 58px;
 	height: 58px;
 	position: sticky !important;
-	top: calc(22px + var(--stickyTop, 0px));
+	top: calc(22px + var(--MI-stickyTop, 0px));
 	left: 0;
 }
 
@@ -831,7 +831,7 @@ function emitUpdReaction(emoji: string, delta: number) {
 	width: 100%;
 	margin-top: 14px;
 	position: sticky;
-	bottom: calc(var(--stickyBottom, 0px) + 14px);
+	bottom: calc(var(--MI-stickyBottom, 0px) + 14px);
 }
 
 .showLessLabel {
@@ -884,7 +884,7 @@ function emitUpdReaction(emoji: string, delta: number) {
 
 .translation {
 	border: solid 0.5px var(--MI_THEME-divider);
-	border-radius: var(--radius);
+	border-radius: var(--MI-radius);
 	padding: 12px;
 	margin-top: 8px;
 }
@@ -998,7 +998,7 @@ function emitUpdReaction(emoji: string, delta: number) {
 		margin: 0 10px 0 0;
 		width: 46px;
 		height: 46px;
-		top: calc(14px + var(--stickyTop, 0px));
+		top: calc(14px + var(--MI-stickyTop, 0px));
 	}
 }
 
diff --git a/packages/frontend/src/components/MkNoteDetailed.vue b/packages/frontend/src/components/MkNoteDetailed.vue
index 8a7a98d23f..6d53685651 100644
--- a/packages/frontend/src/components/MkNoteDetailed.vue
+++ b/packages/frontend/src/components/MkNoteDetailed.vue
@@ -570,7 +570,7 @@ function loadConversation() {
 			width: calc(100% - 8px);
 			height: calc(100% - 8px);
 			border: dashed 2px var(--MI_THEME-focus);
-			border-radius: var(--radius);
+			border-radius: var(--MI-radius);
 			box-sizing: border-box;
 		}
 	}
@@ -711,7 +711,7 @@ function loadConversation() {
 
 .translation {
 	border: solid 0.5px var(--MI_THEME-divider);
-	border-radius: var(--radius);
+	border-radius: var(--MI-radius);
 	padding: 12px;
 	margin-top: 8px;
 }
diff --git a/packages/frontend/src/components/MkNoteSimple.vue b/packages/frontend/src/components/MkNoteSimple.vue
index c3f3c42b42..e684cf2a30 100644
--- a/packages/frontend/src/components/MkNoteSimple.vue
+++ b/packages/frontend/src/components/MkNoteSimple.vue
@@ -51,7 +51,7 @@ const showContent = ref(false);
 	height: 34px;
 	border-radius: 8px;
 	position: sticky !important;
-	top: calc(16px + var(--stickyTop, 0px));
+	top: calc(16px + var(--MI-stickyTop, 0px));
 	left: 0;
 }
 
diff --git a/packages/frontend/src/components/MkNotes.vue b/packages/frontend/src/components/MkNotes.vue
index cb240160cf..1c17c6b691 100644
--- a/packages/frontend/src/components/MkNotes.vue
+++ b/packages/frontend/src/components/MkNotes.vue
@@ -66,7 +66,7 @@ defineExpose({
 
 			.note {
 				background: var(--MI_THEME-panel);
-				border-radius: var(--radius);
+				border-radius: var(--MI-radius);
 			}
 		}
 	}
diff --git a/packages/frontend/src/components/MkOmit.vue b/packages/frontend/src/components/MkOmit.vue
index 38c8664575..a05176e2f4 100644
--- a/packages/frontend/src/components/MkOmit.vue
+++ b/packages/frontend/src/components/MkOmit.vue
@@ -47,7 +47,7 @@ onUnmounted(() => {
 
 <style lang="scss" module>
 .content {
-	--stickyTop: 0px;
+	--MI-stickyTop: 0px;
 
 	&.omitted {
 		position: relative;
diff --git a/packages/frontend/src/components/MkPagePreview.vue b/packages/frontend/src/components/MkPagePreview.vue
index b5281d8a3d..19579cc4cc 100644
--- a/packages/frontend/src/components/MkPagePreview.vue
+++ b/packages/frontend/src/components/MkPagePreview.vue
@@ -42,7 +42,7 @@ const props = defineProps<{
 .eyeCatchingImageRoot {
 	width: 100%;
 	height: 200px;
-	border-radius: var(--radius) var(--radius) 0 0;
+	border-radius: var(--MI-radius) var(--MI-radius) 0 0;
 	overflow: hidden;
 }
 </style>
@@ -67,7 +67,7 @@ const props = defineProps<{
 			left: 0;
 			width: 100%;
 			height: 100%;
-			border-radius: var(--radius);
+			border-radius: var(--MI-radius);
 			pointer-events: none;
 			box-shadow: inset 0 0 0 2px var(--MI_THEME-focus);
 		}
@@ -75,14 +75,14 @@ const props = defineProps<{
 
 	> .thumbnail {
 		& + article {
-			border-radius: 0 0 var(--radius) var(--radius);
+			border-radius: 0 0 var(--MI-radius) var(--MI-radius);
 		}
 	}
 
 	> article {
 		background-color: var(--MI_THEME-panel);
 		padding: 16px;
-		border-radius: var(--radius);
+		border-radius: var(--MI-radius);
 
 		> header {
 			margin-bottom: 8px;
diff --git a/packages/frontend/src/components/MkPageWindow.vue b/packages/frontend/src/components/MkPageWindow.vue
index 421051f73d..4777da2848 100644
--- a/packages/frontend/src/components/MkPageWindow.vue
+++ b/packages/frontend/src/components/MkPageWindow.vue
@@ -181,6 +181,6 @@ defineExpose({
 	min-height: 100%;
 	background: var(--MI_THEME-bg);
 
-	--margin: var(--marginHalf);
+	--MI-margin: var(--MI-marginHalf);
 }
 </style>
diff --git a/packages/frontend/src/components/MkRemoteCaution.vue b/packages/frontend/src/components/MkRemoteCaution.vue
index 3ffb50dbd9..a56a4b1671 100644
--- a/packages/frontend/src/components/MkRemoteCaution.vue
+++ b/packages/frontend/src/components/MkRemoteCaution.vue
@@ -21,7 +21,7 @@ defineProps<{
 	padding: 16px;
 	background: var(--MI_THEME-infoWarnBg);
 	color: var(--MI_THEME-infoWarnFg);
-	border-radius: var(--radius);
+	border-radius: var(--MI-radius);
 	overflow: clip;
 }
 
diff --git a/packages/frontend/src/components/MkSigninDialog.vue b/packages/frontend/src/components/MkSigninDialog.vue
index 51dea960aa..676a336ec7 100644
--- a/packages/frontend/src/components/MkSigninDialog.vue
+++ b/packages/frontend/src/components/MkSigninDialog.vue
@@ -70,7 +70,7 @@ function onLogin(res: Misskey.entities.SigninFlowResponse & { finished: true })
 	max-height: 450px;
 	box-sizing: border-box;
 	background: var(--MI_THEME-panel);
-	border-radius: var(--radius);
+	border-radius: var(--MI-radius);
 }
 
 .header {
@@ -83,7 +83,7 @@ function onLogin(res: Misskey.entities.SigninFlowResponse & { finished: true })
 	display: flex;
 	align-items: center;
 	font-weight: bold;
-	backdrop-filter: var(--blur, blur(15px));
+	backdrop-filter: var(--MI-blur, blur(15px));
 	background: var(--MI_THEME-acrylicBg);
 	z-index: 1;
 }
diff --git a/packages/frontend/src/components/MkSignupDialog.rules.vue b/packages/frontend/src/components/MkSignupDialog.rules.vue
index 1470f1e57e..e2a06dd91f 100644
--- a/packages/frontend/src/components/MkSignupDialog.rules.vue
+++ b/packages/frontend/src/components/MkSignupDialog.rules.vue
@@ -170,7 +170,7 @@ async function updateAgreeNote(v: boolean) {
 		flex-shrink: 0;
 		display: flex;
 		position: sticky;
-		top: calc(var(--stickyTop, 0px) + 8px);
+		top: calc(var(--MI-stickyTop, 0px) + 8px);
 		counter-increment: item;
 		content: counter(item);
 		width: 32px;
diff --git a/packages/frontend/src/components/MkSourceCodeAvailablePopup.vue b/packages/frontend/src/components/MkSourceCodeAvailablePopup.vue
index 438dd7e3a5..4c197ed43e 100644
--- a/packages/frontend/src/components/MkSourceCodeAvailablePopup.vue
+++ b/packages/frontend/src/components/MkSourceCodeAvailablePopup.vue
@@ -63,12 +63,12 @@ function close() {
 .root {
 	position: fixed;
 	z-index: v-bind(zIndex);
-	bottom: var(--margin);
+	bottom: var(--MI-margin);
 	left: 0;
 	right: 0;
 	margin: auto;
 	box-sizing: border-box;
-	width: calc(100% - (var(--margin) * 2));
+	width: calc(100% - (var(--MI-margin) * 2));
 	max-width: 500px;
 	display: flex;
 }
diff --git a/packages/frontend/src/components/MkSubNoteContent.vue b/packages/frontend/src/components/MkSubNoteContent.vue
index a36765b73c..9e02884b8c 100644
--- a/packages/frontend/src/components/MkSubNoteContent.vue
+++ b/packages/frontend/src/components/MkSubNoteContent.vue
@@ -97,7 +97,7 @@ const collapsed = ref(isLong);
 	width: 100%;
 	margin-top: 14px;
 	position: sticky;
-	bottom: calc(var(--stickyBottom, 0px) + 14px);
+	bottom: calc(var(--MI-stickyBottom, 0px) + 14px);
 }
 
 .showLessLabel {
diff --git a/packages/frontend/src/components/MkSystemWebhookEditor.vue b/packages/frontend/src/components/MkSystemWebhookEditor.vue
index 23130d7f9f..a00cf0d9d3 100644
--- a/packages/frontend/src/components/MkSystemWebhookEditor.vue
+++ b/packages/frontend/src/components/MkSystemWebhookEditor.vue
@@ -263,8 +263,8 @@ onMounted(async () => {
 	padding: 12px;
 	border-top: solid 0.5px var(--MI_THEME-divider);
 	background: var(--MI_THEME-acrylicBg);
-	-webkit-backdrop-filter: var(--blur, blur(15px));
-	backdrop-filter: var(--blur, blur(15px));
+	-webkit-backdrop-filter: var(--MI-blur, blur(15px));
+	backdrop-filter: var(--MI-blur, blur(15px));
 }
 
 .switchBox {
diff --git a/packages/frontend/src/components/MkTextarea.vue b/packages/frontend/src/components/MkTextarea.vue
index 0139712232..d1a6e1ebbf 100644
--- a/packages/frontend/src/components/MkTextarea.vue
+++ b/packages/frontend/src/components/MkTextarea.vue
@@ -226,7 +226,7 @@ onUnmounted(() => {
 
 .mfmPreview {
   padding: 12px;
-  border-radius: var(--radius);
+  border-radius: var(--MI-radius);
   box-sizing: border-box;
   min-height: 130px;
 	pointer-events: none;
diff --git a/packages/frontend/src/components/MkTokenGenerateWindow.vue b/packages/frontend/src/components/MkTokenGenerateWindow.vue
index 63dc93ae27..a7bc3f37f1 100644
--- a/packages/frontend/src/components/MkTokenGenerateWindow.vue
+++ b/packages/frontend/src/components/MkTokenGenerateWindow.vue
@@ -137,7 +137,7 @@ function enableAll(): void {
 	margin: 8px -6px 0;
 	padding: 24px 6px 6px;
 	border: 2px solid var(--MI_THEME-error);
-	border-radius: calc(var(--radius) / 2);
+	border-radius: calc(var(--MI-radius) / 2);
 }
 
 .adminPermissionsHeader {
diff --git a/packages/frontend/src/components/MkTutorialDialog.Note.vue b/packages/frontend/src/components/MkTutorialDialog.Note.vue
index 5644907434..b26a01737e 100644
--- a/packages/frontend/src/components/MkTutorialDialog.Note.vue
+++ b/packages/frontend/src/components/MkTutorialDialog.Note.vue
@@ -105,7 +105,7 @@ function removeReaction(emoji) {
 
 <style lang="scss" module>
 .exampleNoteRoot {
-	border-radius: var(--radius);
+	border-radius: var(--MI-radius);
 	border: var(--MI_THEME-panelBorder);
 	background: var(--MI_THEME-panel);
 }
diff --git a/packages/frontend/src/components/MkTutorialDialog.PostNote.vue b/packages/frontend/src/components/MkTutorialDialog.PostNote.vue
index 7044e05804..6559307a94 100644
--- a/packages/frontend/src/components/MkTutorialDialog.PostNote.vue
+++ b/packages/frontend/src/components/MkTutorialDialog.PostNote.vue
@@ -81,7 +81,7 @@ const exampleCWNote = reactive<Misskey.entities.Note>({
 <style lang="scss" module>
 .exampleRoot {
 	max-width: none!important;
-	border-radius: var(--radius);
+	border-radius: var(--MI-radius);
 	border: var(--MI_THEME-panelBorder);
 	background: var(--MI_THEME-panel);
 }
diff --git a/packages/frontend/src/components/MkTutorialDialog.Sensitive.vue b/packages/frontend/src/components/MkTutorialDialog.Sensitive.vue
index ce06b97b6b..f7b60fbc45 100644
--- a/packages/frontend/src/components/MkTutorialDialog.Sensitive.vue
+++ b/packages/frontend/src/components/MkTutorialDialog.Sensitive.vue
@@ -91,7 +91,7 @@ const exampleNote = reactive<Misskey.entities.Note>({
 
 <style lang="scss" module>
 .exampleRoot {
-	border-radius: var(--radius);
+	border-radius: var(--MI-radius);
 	border: var(--MI_THEME-panelBorder);
 	background: var(--MI_THEME-panel);
 }
diff --git a/packages/frontend/src/components/MkTutorialDialog.Timeline.vue b/packages/frontend/src/components/MkTutorialDialog.Timeline.vue
index 9e33afbb53..b203110ef0 100644
--- a/packages/frontend/src/components/MkTutorialDialog.Timeline.vue
+++ b/packages/frontend/src/components/MkTutorialDialog.Timeline.vue
@@ -31,7 +31,7 @@ import { basicTimelineIconClass, basicTimelineTypes } from '@/timelines.js';
 
 <style lang="scss" module>
 .exampleNoteRoot {
-	border-radius: var(--radius);
+	border-radius: var(--MI-radius);
 	border: var(--MI_THEME-panelBorder);
 	background: var(--MI_THEME-panel);
 }
diff --git a/packages/frontend/src/components/MkUpdated.vue b/packages/frontend/src/components/MkUpdated.vue
index fe50ab8cff..c937b4ce59 100644
--- a/packages/frontend/src/components/MkUpdated.vue
+++ b/packages/frontend/src/components/MkUpdated.vue
@@ -47,7 +47,7 @@ onMounted(() => {
 	box-sizing: border-box;
 	text-align: center;
 	background: var(--MI_THEME-panel);
-	border-radius: var(--radius);
+	border-radius: var(--MI-radius);
 }
 
 .title {
diff --git a/packages/frontend/src/components/MkUserAnnouncementEditDialog.vue b/packages/frontend/src/components/MkUserAnnouncementEditDialog.vue
index 26ba108244..7a2b5f5ddc 100644
--- a/packages/frontend/src/components/MkUserAnnouncementEditDialog.vue
+++ b/packages/frontend/src/components/MkUserAnnouncementEditDialog.vue
@@ -142,7 +142,7 @@ async function del() {
 	left: 0;
 	padding: 12px;
 	border-top: solid 0.5px var(--MI_THEME-divider);
-	-webkit-backdrop-filter: var(--blur, blur(15px));
-	backdrop-filter: var(--blur, blur(15px));
+	-webkit-backdrop-filter: var(--MI-blur, blur(15px));
+	backdrop-filter: var(--MI-blur, blur(15px));
 }
 </style>
diff --git a/packages/frontend/src/components/MkUserList.vue b/packages/frontend/src/components/MkUserList.vue
index 17a9254d01..8b4afd7994 100644
--- a/packages/frontend/src/components/MkUserList.vue
+++ b/packages/frontend/src/components/MkUserList.vue
@@ -39,6 +39,6 @@ const props = withDefaults(defineProps<{
 .root {
 	display: grid;
 	grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
-	grid-gap: var(--margin);
+	grid-gap: var(--MI-margin);
 }
 </style>
diff --git a/packages/frontend/src/components/MkUserSetupDialog.Follow.vue b/packages/frontend/src/components/MkUserSetupDialog.Follow.vue
index 1524ea0ec9..5153c06139 100644
--- a/packages/frontend/src/components/MkUserSetupDialog.Follow.vue
+++ b/packages/frontend/src/components/MkUserSetupDialog.Follow.vue
@@ -62,7 +62,7 @@ const popularUsers: Paging = {
 .users {
 	display: grid;
 	grid-template-columns: repeat(auto-fill, minmax(230px, 1fr));
-	grid-gap: var(--margin);
+	grid-gap: var(--MI-margin);
 	justify-content: center;
 }
 </style>
diff --git a/packages/frontend/src/components/MkVisitorDashboard.vue b/packages/frontend/src/components/MkVisitorDashboard.vue
index 91e2898798..97c765d81c 100644
--- a/packages/frontend/src/components/MkVisitorDashboard.vue
+++ b/packages/frontend/src/components/MkVisitorDashboard.vue
@@ -107,7 +107,7 @@ function showMenu(ev: MouseEvent) {
 .panel {
 	position: relative;
 	background: var(--MI_THEME-panel);
-	border-radius: var(--radius);
+	border-radius: var(--MI-radius);
 	box-shadow: 0 12px 32px rgb(0 0 0 / 25%);
 }
 
diff --git a/packages/frontend/src/components/MkWaitingDialog.vue b/packages/frontend/src/components/MkWaitingDialog.vue
index 62e187f172..34fa6b0723 100644
--- a/packages/frontend/src/components/MkWaitingDialog.vue
+++ b/packages/frontend/src/components/MkWaitingDialog.vue
@@ -48,7 +48,7 @@ watch(() => props.showing, () => {
 	box-sizing: border-box;
 	text-align: center;
 	background: var(--MI_THEME-panel);
-	border-radius: var(--radius);
+	border-radius: var(--MI-radius);
 	width: 250px;
 
 	&.iconOnly {
diff --git a/packages/frontend/src/components/MkWidgets.vue b/packages/frontend/src/components/MkWidgets.vue
index 0c51cfa9ce..492dd4cdc0 100644
--- a/packages/frontend/src/components/MkWidgets.vue
+++ b/packages/frontend/src/components/MkWidgets.vue
@@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 <div :class="$style.root">
 	<template v-if="edit">
 		<header :class="$style.editHeader">
-			<MkSelect v-model="widgetAdderSelected" style="margin-bottom: var(--margin)" data-cy-widget-select>
+			<MkSelect v-model="widgetAdderSelected" style="margin-bottom: var(--MI-margin)" data-cy-widget-select>
 				<template #label>{{ i18n.ts.selectWidget }}</template>
 				<option v-for="widget in widgetDefs" :key="widget" :value="widget">{{ i18n.ts._widgets[widget] }}</option>
 			</MkSelect>
@@ -123,7 +123,7 @@ function onContextmenu(widget: Widget, ev: MouseEvent) {
 
 .widget {
 	contain: content;
-	margin: var(--margin) 0;
+	margin: var(--MI-margin) 0;
 
 	&:first-of-type {
 		margin-top: 0;
diff --git a/packages/frontend/src/components/MkWindow.vue b/packages/frontend/src/components/MkWindow.vue
index dd92952a35..a5f7a2e9e5 100644
--- a/packages/frontend/src/components/MkWindow.vue
+++ b/packages/frontend/src/components/MkWindow.vue
@@ -502,7 +502,7 @@ defineExpose({
 	contain: content;
 	width: 100%;
 	height: 100%;
-	border-radius: var(--radius);
+	border-radius: var(--MI-radius);
 }
 
 .header {
@@ -515,8 +515,8 @@ defineExpose({
 	user-select: none;
 	height: var(--height);
 	background: var(--MI_THEME-windowHeader);
-	-webkit-backdrop-filter: var(--blur, blur(15px));
-	backdrop-filter: var(--blur, blur(15px));
+	-webkit-backdrop-filter: var(--MI-blur, blur(15px));
+	backdrop-filter: var(--MI-blur, blur(15px));
 	//border-bottom: solid 1px var(--MI_THEME-divider);
 	font-size: 90%;
 	font-weight: bold;
diff --git a/packages/frontend/src/components/global/MkPageHeader.vue b/packages/frontend/src/components/global/MkPageHeader.vue
index e032313b02..aa4be69b2c 100644
--- a/packages/frontend/src/components/global/MkPageHeader.vue
+++ b/packages/frontend/src/components/global/MkPageHeader.vue
@@ -130,8 +130,8 @@ onUnmounted(() => {
 
 <style lang="scss" module>
 .root {
-	-webkit-backdrop-filter: var(--blur, blur(15px));
-	backdrop-filter: var(--blur, blur(15px));
+	-webkit-backdrop-filter: var(--MI-blur, blur(15px));
+	backdrop-filter: var(--MI-blur, blur(15px));
 	border-bottom: solid 0.5px var(--MI_THEME-divider);
 	width: 100%;
 }
@@ -145,7 +145,7 @@ onUnmounted(() => {
 .upper {
 	--height: 50px;
 	display: flex;
-	gap: var(--margin);
+	gap: var(--MI-margin);
 	height: var(--height);
 
 	.tabs:first-child {
diff --git a/packages/frontend/src/components/global/MkStickyContainer.vue b/packages/frontend/src/components/global/MkStickyContainer.vue
index 72993991ce..cb21dafd2b 100644
--- a/packages/frontend/src/components/global/MkStickyContainer.vue
+++ b/packages/frontend/src/components/global/MkStickyContainer.vue
@@ -69,28 +69,28 @@ onMounted(() => {
 
 	watch(childStickyTop, () => {
 		if (bodyEl.value == null) return;
-		bodyEl.value.style.setProperty('--stickyTop', `${childStickyTop.value}px`);
+		bodyEl.value.style.setProperty('--MI-stickyTop', `${childStickyTop.value}px`);
 	}, {
 		immediate: true,
 	});
 
 	watch(childStickyBottom, () => {
 		if (bodyEl.value == null) return;
-		bodyEl.value.style.setProperty('--stickyBottom', `${childStickyBottom.value}px`);
+		bodyEl.value.style.setProperty('--MI-stickyBottom', `${childStickyBottom.value}px`);
 	}, {
 		immediate: true,
 	});
 
 	if (headerEl.value != null) {
 		headerEl.value.style.position = 'sticky';
-		headerEl.value.style.top = 'var(--stickyTop, 0)';
+		headerEl.value.style.top = 'var(--MI-stickyTop, 0)';
 		headerEl.value.style.zIndex = '1';
 		observer.observe(headerEl.value);
 	}
 
 	if (footerEl.value != null) {
 		footerEl.value.style.position = 'sticky';
-		footerEl.value.style.bottom = 'var(--stickyBottom, 0)';
+		footerEl.value.style.bottom = 'var(--MI-stickyBottom, 0)';
 		footerEl.value.style.zIndex = '1';
 		observer.observe(footerEl.value);
 	}
diff --git a/packages/frontend/src/components/page/page.dynamic.vue b/packages/frontend/src/components/page/page.dynamic.vue
index c355cd07d7..c2449931c1 100644
--- a/packages/frontend/src/components/page/page.dynamic.vue
+++ b/packages/frontend/src/components/page/page.dynamic.vue
@@ -28,8 +28,8 @@ const props = defineProps<{
 <style lang="scss" module>
 .root {
 	border: 1px solid var(--MI_THEME-divider);
-	border-radius: var(--radius);
-	padding: var(--margin);
+	border-radius: var(--MI-radius);
+	padding: var(--MI-margin);
 	text-align: center;
 }
 
diff --git a/packages/frontend/src/components/page/page.image.vue b/packages/frontend/src/components/page/page.image.vue
index c4bedcdb54..69443ce7dd 100644
--- a/packages/frontend/src/components/page/page.image.vue
+++ b/packages/frontend/src/components/page/page.image.vue
@@ -29,7 +29,7 @@ onMounted(() => {
 <style lang="scss" module>
 .root {
 	border: 1px solid var(--MI_THEME-divider);
-	border-radius: var(--radius);
+	border-radius: var(--MI-radius);
 	overflow: hidden;
 }
 .mediaList {
diff --git a/packages/frontend/src/components/page/page.note.vue b/packages/frontend/src/components/page/page.note.vue
index 4a1be9b772..84436e7adb 100644
--- a/packages/frontend/src/components/page/page.note.vue
+++ b/packages/frontend/src/components/page/page.note.vue
@@ -36,6 +36,6 @@ onMounted(() => {
 <style lang="scss" module>
 .root {
 	border: 1px solid var(--MI_THEME-divider);
-	border-radius: var(--radius);
+	border-radius: var(--MI-radius);
 }
 </style>
diff --git a/packages/frontend/src/pages/about-misskey.vue b/packages/frontend/src/pages/about-misskey.vue
index a66d580db9..891489f1a1 100644
--- a/packages/frontend/src/pages/about-misskey.vue
+++ b/packages/frontend/src/pages/about-misskey.vue
@@ -441,7 +441,7 @@ definePageMetadata(() => ({
 .znqjceqz {
 	> .about {
 		position: relative;
-		border-radius: var(--radius);
+		border-radius: var(--MI-radius);
 
 		> .treasure {
 			position: absolute;
diff --git a/packages/frontend/src/pages/about.federation.vue b/packages/frontend/src/pages/about.federation.vue
index b3776c67e6..0a7cb8a50b 100644
--- a/packages/frontend/src/pages/about.federation.vue
+++ b/packages/frontend/src/pages/about.federation.vue
@@ -10,7 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 			<template #prefix><i class="ti ti-search"></i></template>
 			<template #label>{{ i18n.ts.host }}</template>
 		</MkInput>
-		<FormSplit style="margin-top: var(--margin);">
+		<FormSplit style="margin-top: var(--MI-margin);">
 			<MkSelect v-model="state">
 				<template #label>{{ i18n.ts.state }}</template>
 				<option value="all">{{ i18n.ts.all }}</option>
diff --git a/packages/frontend/src/pages/about.overview.vue b/packages/frontend/src/pages/about.overview.vue
index c19757f88f..e5e57c05c4 100644
--- a/packages/frontend/src/pages/about.overview.vue
+++ b/packages/frontend/src/pages/about.overview.vue
@@ -183,7 +183,7 @@ const initStats = () => misskeyApi('stats', {});
 		flex-shrink: 0;
 		display: flex;
 		position: sticky;
-		top: calc(var(--stickyTop, 0px) + 8px);
+		top: calc(var(--MI-stickyTop, 0px) + 8px);
 		counter-increment: item;
 		content: counter(item);
 		width: 32px;
diff --git a/packages/frontend/src/pages/admin/RolesEditorFormula.vue b/packages/frontend/src/pages/admin/RolesEditorFormula.vue
index dc2862d225..4762ef3f97 100644
--- a/packages/frontend/src/pages/admin/RolesEditorFormula.vue
+++ b/packages/frontend/src/pages/admin/RolesEditorFormula.vue
@@ -156,7 +156,7 @@ function removeSelf() {
 
 .item {
 	border: solid 2px var(--MI_THEME-divider);
-	border-radius: var(--radius);
+	border-radius: var(--MI-radius);
 	padding: 12px;
 
 	&:hover {
diff --git a/packages/frontend/src/pages/admin/_header_.vue b/packages/frontend/src/pages/admin/_header_.vue
index 36fe483771..9b1bf51f58 100644
--- a/packages/frontend/src/pages/admin/_header_.vue
+++ b/packages/frontend/src/pages/admin/_header_.vue
@@ -156,8 +156,8 @@ onUnmounted(() => {
 	--height: 60px;
 	display: flex;
 	width: 100%;
-	-webkit-backdrop-filter: var(--blur, blur(15px));
-	backdrop-filter: var(--blur, blur(15px));
+	-webkit-backdrop-filter: var(--MI-blur, blur(15px));
+	backdrop-filter: var(--MI-blur, blur(15px));
 
 	> .buttons {
 		--margin: 8px;
diff --git a/packages/frontend/src/pages/admin/abuse-report/notification-recipient.editor.vue b/packages/frontend/src/pages/admin/abuse-report/notification-recipient.editor.vue
index f70b46b84a..eef24afd32 100644
--- a/packages/frontend/src/pages/admin/abuse-report/notification-recipient.editor.vue
+++ b/packages/frontend/src/pages/admin/abuse-report/notification-recipient.editor.vue
@@ -296,8 +296,8 @@ onMounted(async () => {
 	padding: 12px;
 	border-top: solid 0.5px var(--MI_THEME-divider);
 	background: var(--MI_THEME-acrylicBg);
-	-webkit-backdrop-filter: var(--blur, blur(15px));
-	backdrop-filter: var(--blur, blur(15px));
+	-webkit-backdrop-filter: var(--MI-blur, blur(15px));
+	backdrop-filter: var(--MI-blur, blur(15px));
 }
 
 .systemWebhook {
diff --git a/packages/frontend/src/pages/admin/ads.vue b/packages/frontend/src/pages/admin/ads.vue
index 6c8901b10b..0d67359e47 100644
--- a/packages/frontend/src/pages/admin/ads.vue
+++ b/packages/frontend/src/pages/admin/ads.vue
@@ -266,7 +266,7 @@ definePageMetadata(() => ({
 	padding: 32px;
 
 	&:not(:last-child) {
-		margin-bottom: var(--margin);
+		margin-bottom: var(--MI-margin);
 	}
 }
 .input {
diff --git a/packages/frontend/src/pages/admin/branding.vue b/packages/frontend/src/pages/admin/branding.vue
index 947dde767e..95f82c1f24 100644
--- a/packages/frontend/src/pages/admin/branding.vue
+++ b/packages/frontend/src/pages/admin/branding.vue
@@ -183,7 +183,7 @@ definePageMetadata(() => ({
 
 <style lang="scss" module>
 .footer {
-	-webkit-backdrop-filter: var(--blur, blur(15px));
-	backdrop-filter: var(--blur, blur(15px));
+	-webkit-backdrop-filter: var(--MI-blur, blur(15px));
+	backdrop-filter: var(--MI-blur, blur(15px));
 }
 </style>
diff --git a/packages/frontend/src/pages/admin/email-settings.vue b/packages/frontend/src/pages/admin/email-settings.vue
index 4a858887f3..5b60e67dac 100644
--- a/packages/frontend/src/pages/admin/email-settings.vue
+++ b/packages/frontend/src/pages/admin/email-settings.vue
@@ -138,7 +138,7 @@ definePageMetadata(() => ({
 
 <style lang="scss" module>
 .footer {
-	-webkit-backdrop-filter: var(--blur, blur(15px));
-	backdrop-filter: var(--blur, blur(15px));
+	-webkit-backdrop-filter: var(--MI-blur, blur(15px));
+	backdrop-filter: var(--MI-blur, blur(15px));
 }
 </style>
diff --git a/packages/frontend/src/pages/admin/federation.vue b/packages/frontend/src/pages/admin/federation.vue
index debf684c9b..e7b9fd8621 100644
--- a/packages/frontend/src/pages/admin/federation.vue
+++ b/packages/frontend/src/pages/admin/federation.vue
@@ -14,7 +14,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 						<template #prefix><i class="ti ti-search"></i></template>
 						<template #label>{{ i18n.ts.host }}</template>
 					</MkInput>
-					<FormSplit style="margin-top: var(--margin);">
+					<FormSplit style="margin-top: var(--MI-margin);">
 						<MkSelect v-model="state">
 							<template #label>{{ i18n.ts.state }}</template>
 							<option value="all">{{ i18n.ts.all }}</option>
diff --git a/packages/frontend/src/pages/admin/files.vue b/packages/frontend/src/pages/admin/files.vue
index 5132b85c64..4cc859227f 100644
--- a/packages/frontend/src/pages/admin/files.vue
+++ b/packages/frontend/src/pages/admin/files.vue
@@ -9,7 +9,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 		<template #header><XHeader :actions="headerActions"/></template>
 		<MkSpacer :contentMax="900">
 			<div class="_gaps">
-				<div class="inputs" style="display: flex; gap: var(--margin); flex-wrap: wrap;">
+				<div class="inputs" style="display: flex; gap: var(--MI-margin); flex-wrap: wrap;">
 					<MkSelect v-model="origin" style="margin: 0; flex: 1;">
 						<template #label>{{ i18n.ts.instance }}</template>
 						<option value="combined">{{ i18n.ts.all }}</option>
@@ -20,7 +20,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 						<template #label>{{ i18n.ts.host }}</template>
 					</MkInput>
 				</div>
-				<div class="inputs" style="display: flex; gap: var(--margin); flex-wrap: wrap;">
+				<div class="inputs" style="display: flex; gap: var(--MI-margin); flex-wrap: wrap;">
 					<MkInput v-model="userId" :debounce="true" type="search" style="margin: 0; flex: 1;">
 						<template #label>User ID</template>
 					</MkInput>
diff --git a/packages/frontend/src/pages/admin/modlog.ModLog.vue b/packages/frontend/src/pages/admin/modlog.ModLog.vue
index ddbd293c3a..1e144394fb 100644
--- a/packages/frontend/src/pages/admin/modlog.ModLog.vue
+++ b/packages/frontend/src/pages/admin/modlog.ModLog.vue
@@ -89,7 +89,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 	</template>
 
 	<div>
-		<div style="display: flex; gap: var(--margin); flex-wrap: wrap;">
+		<div style="display: flex; gap: var(--MI-margin); flex-wrap: wrap;">
 			<div style="flex: 1;">{{ i18n.ts.moderator }}: <MkA :to="`/admin/user/${log.userId}`" class="_link">@{{ log.user?.username }}</MkA></div>
 			<div style="flex: 1;">{{ i18n.ts.dateAndTime }}: <MkTime :time="log.createdAt" mode="detail"/></div>
 		</div>
diff --git a/packages/frontend/src/pages/admin/modlog.vue b/packages/frontend/src/pages/admin/modlog.vue
index 38610e7e92..c9eaf07531 100644
--- a/packages/frontend/src/pages/admin/modlog.vue
+++ b/packages/frontend/src/pages/admin/modlog.vue
@@ -8,7 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 	<template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template>
 	<MkSpacer :contentMax="900">
 		<div>
-			<div style="display: flex; gap: var(--margin); flex-wrap: wrap;">
+			<div style="display: flex; gap: var(--MI-margin); flex-wrap: wrap;">
 				<MkSelect v-model="type" style="margin: 0; flex: 1;">
 					<template #label>{{ i18n.ts.type }}</template>
 					<option :value="null">{{ i18n.ts.all }}</option>
@@ -19,8 +19,8 @@ SPDX-License-Identifier: AGPL-3.0-only
 				</MkInput>
 			</div>
 
-			<MkPagination v-slot="{items}" ref="logs" :pagination="pagination" style="margin-top: var(--margin);">
-				<MkDateSeparatedList v-slot="{ item }" :items="items" :noGap="false" style="--margin: 8px;">
+			<MkPagination v-slot="{items}" ref="logs" :pagination="pagination" style="margin-top: var(--MI-margin);">
+				<MkDateSeparatedList v-slot="{ item }" :items="items" :noGap="false" style="--MI-margin: 8px;">
 					<XModLog :key="item.id" :log="item"/>
 				</MkDateSeparatedList>
 			</MkPagination>
diff --git a/packages/frontend/src/pages/admin/object-storage.vue b/packages/frontend/src/pages/admin/object-storage.vue
index 5fddb715cd..d5a664934c 100644
--- a/packages/frontend/src/pages/admin/object-storage.vue
+++ b/packages/frontend/src/pages/admin/object-storage.vue
@@ -157,7 +157,7 @@ definePageMetadata(() => ({
 
 <style lang="scss" module>
 .footer {
-	-webkit-backdrop-filter: var(--blur, blur(15px));
-	backdrop-filter: var(--blur, blur(15px));
+	-webkit-backdrop-filter: var(--MI-blur, blur(15px));
+	backdrop-filter: var(--MI-blur, blur(15px));
 }
 </style>
diff --git a/packages/frontend/src/pages/admin/overview.queue.vue b/packages/frontend/src/pages/admin/overview.queue.vue
index 98d1b8d7f6..de6b254412 100644
--- a/packages/frontend/src/pages/admin/overview.queue.vue
+++ b/packages/frontend/src/pages/admin/overview.queue.vue
@@ -120,7 +120,7 @@ onUnmounted(() => {
 				min-width: 0;
 				padding: 16px;
 				background: var(--MI_THEME-panel);
-				border-radius: var(--radius);
+				border-radius: var(--MI-radius);
 
 				> .title {
 					font-size: 0.85em;
diff --git a/packages/frontend/src/pages/admin/queue.chart.vue b/packages/frontend/src/pages/admin/queue.chart.vue
index 700865c91c..7c171ba0e1 100644
--- a/packages/frontend/src/pages/admin/queue.chart.vue
+++ b/packages/frontend/src/pages/admin/queue.chart.vue
@@ -136,7 +136,7 @@ onUnmounted(() => {
 	min-width: 0;
 	padding: 16px;
 	background: var(--MI_THEME-panel);
-	border-radius: var(--radius);
+	border-radius: var(--MI-radius);
 }
 
 .chartTitle {
diff --git a/packages/frontend/src/pages/admin/roles.edit.vue b/packages/frontend/src/pages/admin/roles.edit.vue
index 60f06d50ba..2b4006c3f7 100644
--- a/packages/frontend/src/pages/admin/roles.edit.vue
+++ b/packages/frontend/src/pages/admin/roles.edit.vue
@@ -95,7 +95,7 @@ definePageMetadata(() => ({
 
 <style lang="scss" module>
 .footer {
-	-webkit-backdrop-filter: var(--blur, blur(15px));
-	backdrop-filter: var(--blur, blur(15px));
+	-webkit-backdrop-filter: var(--MI-blur, blur(15px));
+	backdrop-filter: var(--MI-blur, blur(15px));
 }
 </style>
diff --git a/packages/frontend/src/pages/antenna-timeline.vue b/packages/frontend/src/pages/antenna-timeline.vue
index 167f402931..a01bafd996 100644
--- a/packages/frontend/src/pages/antenna-timeline.vue
+++ b/packages/frontend/src/pages/antenna-timeline.vue
@@ -97,26 +97,26 @@ definePageMetadata(() => ({
 <style lang="scss" module>
 .new {
 	position: sticky;
-	top: calc(var(--stickyTop, 0px) + 16px);
+	top: calc(var(--MI-stickyTop, 0px) + 16px);
 	z-index: 1000;
 	width: 100%;
 	margin: calc(-0.675em - 8px) 0;
 
 	&:first-child {
-		margin-top: calc(-0.675em - 8px - var(--margin));
+		margin-top: calc(-0.675em - 8px - var(--MI-margin));
 	}
 }
 
 .newButton {
 	display: block;
-	margin: var(--margin) auto 0 auto;
+	margin: var(--MI-margin) auto 0 auto;
 	padding: 8px 16px;
 	border-radius: 32px;
 }
 
 .tl {
 	background: var(--MI_THEME-bg);
-	border-radius: var(--radius);
+	border-radius: var(--MI-radius);
 	overflow: clip;
 }
 </style>
diff --git a/packages/frontend/src/pages/avatar-decorations.vue b/packages/frontend/src/pages/avatar-decorations.vue
index b377314856..b97e7c0eea 100644
--- a/packages/frontend/src/pages/avatar-decorations.vue
+++ b/packages/frontend/src/pages/avatar-decorations.vue
@@ -124,7 +124,7 @@ definePageMetadata(() => ({
 	display: grid;
 	grid-template-columns: 1fr;
 	grid-template-rows: auto auto;
-	gap: var(--margin);
+	gap: var(--MI-margin);
 }
 
 .preview {
@@ -132,7 +132,7 @@ definePageMetadata(() => ({
 	place-items: center;
 	grid-template-columns: 1fr 1fr;
 	grid-template-rows: 1fr;
-	gap: var(--margin);
+	gap: var(--MI-margin);
 }
 
 .previewItem {
@@ -142,7 +142,7 @@ definePageMetadata(() => ({
 	display: flex;
 	align-items: center;
 	justify-content: center;
-	border-radius: var(--radius);
+	border-radius: var(--MI-radius);
 
 	&.light {
 		background: #eee;
@@ -157,7 +157,7 @@ definePageMetadata(() => ({
 	.editorWrapper {
 		grid-template-columns: 200px 1fr;
 		grid-template-rows: 1fr;
-		gap: calc(var(--margin) * 2);
+		gap: calc(var(--MI-margin) * 2);
 	}
 
 	.preview {
diff --git a/packages/frontend/src/pages/channel.vue b/packages/frontend/src/pages/channel.vue
index c8b04ca350..b61054118d 100644
--- a/packages/frontend/src/pages/channel.vue
+++ b/packages/frontend/src/pages/channel.vue
@@ -269,12 +269,12 @@ definePageMetadata(() => ({
 
 <style lang="scss" module>
 .main {
-	min-height: calc(100cqh - (var(--stickyTop, 0px) + var(--stickyBottom, 0px)));
+	min-height: calc(100cqh - (var(--MI-stickyTop, 0px) + var(--MI-stickyBottom, 0px)));
 }
 
 .footer {
-	-webkit-backdrop-filter: var(--blur, blur(15px));
-	backdrop-filter: var(--blur, blur(15px));
+	-webkit-backdrop-filter: var(--MI-blur, blur(15px));
+	backdrop-filter: var(--MI-blur, blur(15px));
 	background: var(--MI_THEME-acrylicBg);
 	border-top: solid 0.5px var(--MI_THEME-divider);
 }
diff --git a/packages/frontend/src/pages/custom-emojis-manager.vue b/packages/frontend/src/pages/custom-emojis-manager.vue
index 6d0b3d8d2e..1e416e22d3 100644
--- a/packages/frontend/src/pages/custom-emojis-manager.vue
+++ b/packages/frontend/src/pages/custom-emojis-manager.vue
@@ -317,14 +317,14 @@ definePageMetadata(() => ({
 .ogwlenmc {
 	> .local {
 		.empty {
-			margin: var(--margin);
+			margin: var(--MI-margin);
 		}
 
 		.ldhfsamy {
 			display: grid;
 			grid-template-columns: repeat(auto-fill, minmax(190px, 1fr));
 			grid-gap: 12px;
-			margin: var(--margin) 0;
+			margin: var(--MI-margin) 0;
 
 			> .emoji {
 				display: flex;
@@ -369,14 +369,14 @@ definePageMetadata(() => ({
 
 	> .remote {
 		.empty {
-			margin: var(--margin);
+			margin: var(--MI-margin);
 		}
 
 		.ldhfsamy {
 			display: grid;
 			grid-template-columns: repeat(auto-fill, minmax(190px, 1fr));
 			grid-gap: 12px;
-			margin: var(--margin) 0;
+			margin: var(--MI-margin) 0;
 
 			> .emoji {
 				display: flex;
diff --git a/packages/frontend/src/pages/drive.file.info.vue b/packages/frontend/src/pages/drive.file.info.vue
index 98fa99e2a3..dfcc82c77b 100644
--- a/packages/frontend/src/pages/drive.file.info.vue
+++ b/packages/frontend/src/pages/drive.file.info.vue
@@ -232,7 +232,7 @@ onMounted(async () => {
 
 .filePreviewRoot {
 	background: var(--MI_THEME-panel);
-	border-radius: var(--radius);
+	border-radius: var(--MI-radius);
 	// MkMediaList 内の上部マージン 4px
 	padding: calc(1rem - 4px) 1rem 1rem;
 }
@@ -285,7 +285,7 @@ onMounted(async () => {
 	align-items: center;
 	min-width: 0;
 	font-weight: 700;
-	border-radius: var(--radius);
+	border-radius: var(--MI-radius);
 	font-size: .8rem;
 
 	>.fileNameEditIcon {
@@ -322,7 +322,7 @@ onMounted(async () => {
 	display: block;
 	width: 100%;
 	padding: .5rem 1rem;
-	border-radius: var(--radius);
+	border-radius: var(--MI-radius);
 
 	.kvEditIcon {
 		display: inline-block;
diff --git a/packages/frontend/src/pages/emoji-edit-dialog.vue b/packages/frontend/src/pages/emoji-edit-dialog.vue
index bd798d9f3a..969aa6bbf7 100644
--- a/packages/frontend/src/pages/emoji-edit-dialog.vue
+++ b/packages/frontend/src/pages/emoji-edit-dialog.vue
@@ -245,7 +245,7 @@ async function del() {
 	padding: 12px;
 	border-top: solid 0.5px var(--MI_THEME-divider);
 	background: var(--MI_THEME-acrylicBg);
-	-webkit-backdrop-filter: var(--blur, blur(15px));
-	backdrop-filter: var(--blur, blur(15px));
+	-webkit-backdrop-filter: var(--MI-blur, blur(15px));
+	backdrop-filter: var(--MI-blur, blur(15px));
 }
 </style>
diff --git a/packages/frontend/src/pages/explore.featured.vue b/packages/frontend/src/pages/explore.featured.vue
index cfdb235d3a..8b16a88ff3 100644
--- a/packages/frontend/src/pages/explore.featured.vue
+++ b/packages/frontend/src/pages/explore.featured.vue
@@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <template>
 <MkSpacer :contentMax="800">
-	<MkTab v-model="tab" style="margin-bottom: var(--margin);">
+	<MkTab v-model="tab" style="margin-bottom: var(--MI-margin);">
 		<option value="notes">{{ i18n.ts.notes }}</option>
 		<option value="polls">{{ i18n.ts.poll }}</option>
 	</MkTab>
diff --git a/packages/frontend/src/pages/explore.users.vue b/packages/frontend/src/pages/explore.users.vue
index e9608ae94e..c9acfec04f 100644
--- a/packages/frontend/src/pages/explore.users.vue
+++ b/packages/frontend/src/pages/explore.users.vue
@@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <template>
 <MkSpacer :contentMax="1200">
-	<MkTab v-model="origin" style="margin-bottom: var(--margin);">
+	<MkTab v-model="origin" style="margin-bottom: var(--MI-margin);">
 		<option value="local">{{ i18n.ts.local }}</option>
 		<option value="remote">{{ i18n.ts.remote }}</option>
 	</MkTab>
diff --git a/packages/frontend/src/pages/favorites.vue b/packages/frontend/src/pages/favorites.vue
index e2765da3e9..6716566101 100644
--- a/packages/frontend/src/pages/favorites.vue
+++ b/packages/frontend/src/pages/favorites.vue
@@ -47,6 +47,6 @@ definePageMetadata(() => ({
 <style lang="scss" module>
 .note {
 	background: var(--MI_THEME-panel);
-	border-radius: var(--radius);
+	border-radius: var(--MI-radius);
 }
 </style>
diff --git a/packages/frontend/src/pages/flash/flash-edit.vue b/packages/frontend/src/pages/flash/flash-edit.vue
index 87bd707f6d..d84ec4873b 100644
--- a/packages/frontend/src/pages/flash/flash-edit.vue
+++ b/packages/frontend/src/pages/flash/flash-edit.vue
@@ -467,7 +467,7 @@ definePageMetadata(() => ({
 </script>
 <style lang="scss" module>
 .footer {
-	backdrop-filter: var(--blur, blur(15px));
+	backdrop-filter: var(--MI-blur, blur(15px));
 	background: var(--MI_THEME-acrylicBg);
 	border-top: solid .5px var(--MI_THEME-divider);
 }
diff --git a/packages/frontend/src/pages/gallery/index.vue b/packages/frontend/src/pages/gallery/index.vue
index e0e187f2ce..f396fd2c0c 100644
--- a/packages/frontend/src/pages/gallery/index.vue
+++ b/packages/frontend/src/pages/gallery/index.vue
@@ -130,6 +130,6 @@ definePageMetadata(() => ({
 	display: grid;
 	grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
 	grid-gap: 12px;
-	margin: 0 var(--margin);
+	margin: 0 var(--MI-margin);
 }
 </style>
diff --git a/packages/frontend/src/pages/gallery/post.vue b/packages/frontend/src/pages/gallery/post.vue
index aab4e53454..feb4c60611 100644
--- a/packages/frontend/src/pages/gallery/post.vue
+++ b/packages/frontend/src/pages/gallery/post.vue
@@ -321,7 +321,7 @@ definePageMetadata(() => ({
 	display: grid;
 	grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
 	grid-gap: 12px;
-	margin: var(--margin);
+	margin: var(--MI-margin);
 
 	> .post {
 
diff --git a/packages/frontend/src/pages/install-extensions.vue b/packages/frontend/src/pages/install-extensions.vue
index 30e658d8c0..6d68ed83b4 100644
--- a/packages/frontend/src/pages/install-extensions.vue
+++ b/packages/frontend/src/pages/install-extensions.vue
@@ -250,7 +250,7 @@ definePageMetadata(() => ({
 
 <style lang="scss" module>
 .extInstallerRoot {
-	border-radius: var(--radius);
+	border-radius: var(--MI-radius);
 	background: var(--MI_THEME-panel);
 	padding: 1.5rem;
 }
diff --git a/packages/frontend/src/pages/list.vue b/packages/frontend/src/pages/list.vue
index 954246ff93..48bc568ac4 100644
--- a/packages/frontend/src/pages/list.vue
+++ b/packages/frontend/src/pages/list.vue
@@ -108,7 +108,7 @@ definePageMetadata(() => ({
 </script>
 <style lang="scss" module>
 .main {
-	min-height: calc(100cqh - (var(--stickyTop, 0px) + var(--stickyBottom, 0px)));
+	min-height: calc(100cqh - (var(--MI-stickyTop, 0px) + var(--MI-stickyBottom, 0px)));
 }
 
 .userItem {
diff --git a/packages/frontend/src/pages/my-lists/list.vue b/packages/frontend/src/pages/my-lists/list.vue
index a78f4bb539..804a5ae8f8 100644
--- a/packages/frontend/src/pages/my-lists/list.vue
+++ b/packages/frontend/src/pages/my-lists/list.vue
@@ -199,7 +199,7 @@ definePageMetadata(() => ({
 
 <style lang="scss" module>
 .main {
-	min-height: calc(100cqh - (var(--stickyTop, 0px) + var(--stickyBottom, 0px)));
+	min-height: calc(100cqh - (var(--MI-stickyTop, 0px) + var(--MI-stickyBottom, 0px)));
 }
 
 .userItem {
@@ -234,8 +234,8 @@ definePageMetadata(() => ({
 }
 
 .footer {
-	-webkit-backdrop-filter: var(--blur, blur(15px));
-	backdrop-filter: var(--blur, blur(15px));
+	-webkit-backdrop-filter: var(--MI-blur, blur(15px));
+	backdrop-filter: var(--MI-blur, blur(15px));
 	border-top: solid 0.5px var(--MI_THEME-divider);
 }
 </style>
diff --git a/packages/frontend/src/pages/note.vue b/packages/frontend/src/pages/note.vue
index d2e7559109..448244204d 100644
--- a/packages/frontend/src/pages/note.vue
+++ b/packages/frontend/src/pages/note.vue
@@ -170,11 +170,11 @@ definePageMetadata(() => ({
 }
 
 .loadNext {
-	margin-bottom: var(--margin);
+	margin-bottom: var(--MI-margin);
 }
 
 .loadPrev {
-	margin-top: var(--margin);
+	margin-top: var(--MI-margin);
 }
 
 .loadButton {
@@ -182,7 +182,7 @@ definePageMetadata(() => ({
 }
 
 .note {
-	border-radius: var(--radius);
+	border-radius: var(--MI-radius);
 	background: var(--MI_THEME-panel);
 }
 </style>
diff --git a/packages/frontend/src/pages/notifications.vue b/packages/frontend/src/pages/notifications.vue
index bd93fc8369..46ee501c76 100644
--- a/packages/frontend/src/pages/notifications.vue
+++ b/packages/frontend/src/pages/notifications.vue
@@ -102,7 +102,7 @@ definePageMetadata(() => ({
 
 <style module lang="scss">
 .notifications {
-	border-radius: var(--radius);
+	border-radius: var(--MI-radius);
 	overflow: clip;
 }
 </style>
diff --git a/packages/frontend/src/pages/page.vue b/packages/frontend/src/pages/page.vue
index 73fe938e9c..a1bec52f18 100644
--- a/packages/frontend/src/pages/page.vue
+++ b/packages/frontend/src/pages/page.vue
@@ -365,7 +365,7 @@ definePageMetadata(() => ({
 }
 
 .pageMain {
-	border-radius: var(--radius);
+	border-radius: var(--MI-radius);
 	padding: 2rem;
 	background: var(--MI_THEME-panel);
 	box-sizing: border-box;
@@ -374,7 +374,7 @@ definePageMetadata(() => ({
 .pageBanner {
 	width: calc(100% + 4rem);
 	margin: -2rem -2rem 1.5rem;
-	border-radius: var(--radius) var(--radius) 0 0;
+	border-radius: var(--MI-radius) var(--MI-radius) 0 0;
 	overflow: hidden;
 	position: relative;
 
@@ -458,7 +458,7 @@ definePageMetadata(() => ({
 			flex-shrink: 0;
 			display: flex;
 			align-items: center;
-			gap: var(--marginHalf);
+			gap: var(--MI-marginHalf);
 			margin-left: auto;
 		}
 	}
@@ -479,7 +479,7 @@ definePageMetadata(() => ({
 	> .other {
 		margin-left: auto;
 		display: flex;
-		gap: var(--marginHalf);
+		gap: var(--MI-marginHalf);
 	}
 }
 
@@ -526,11 +526,11 @@ definePageMetadata(() => ({
 	display: flex;
 	align-items: center;
 	flex-wrap: wrap;
-	gap: var(--marginHalf);
+	gap: var(--MI-marginHalf);
 }
 
 .relatedPagesRoot {
-	padding: var(--margin);
+	padding: var(--MI-margin);
 }
 
 .relatedPagesItem > article {
diff --git a/packages/frontend/src/pages/reversi/game.setting.vue b/packages/frontend/src/pages/reversi/game.setting.vue
index f24614f2eb..dfb6e3f53e 100644
--- a/packages/frontend/src/pages/reversi/game.setting.vue
+++ b/packages/frontend/src/pages/reversi/game.setting.vue
@@ -290,8 +290,8 @@ onUnmounted(() => {
 }
 
 .footer {
-	-webkit-backdrop-filter: var(--blur, blur(15px));
-	backdrop-filter: var(--blur, blur(15px));
+	-webkit-backdrop-filter: var(--MI-blur, blur(15px));
+	backdrop-filter: var(--MI-blur, blur(15px));
 	background: var(--MI_THEME-acrylicBg);
 	border-top: solid 0.5px var(--MI_THEME-divider);
 }
diff --git a/packages/frontend/src/pages/reversi/index.vue b/packages/frontend/src/pages/reversi/index.vue
index 91616d3a50..d608a2411c 100644
--- a/packages/frontend/src/pages/reversi/index.vue
+++ b/packages/frontend/src/pages/reversi/index.vue
@@ -285,7 +285,7 @@ definePageMetadata(() => ({
 .gamePreviews {
 	display: grid;
 	grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
-	grid-gap: var(--margin);
+	grid-gap: var(--MI-margin);
 }
 
 .gamePreview {
diff --git a/packages/frontend/src/pages/scratchpad.vue b/packages/frontend/src/pages/scratchpad.vue
index 280a8d0d44..2250e1ce60 100644
--- a/packages/frontend/src/pages/scratchpad.vue
+++ b/packages/frontend/src/pages/scratchpad.vue
@@ -204,7 +204,7 @@ definePageMetadata(() => ({
 .root {
 	display: flex;
 	flex-direction: column;
-	gap: var(--margin);
+	gap: var(--MI-margin);
 }
 
 .editor {
diff --git a/packages/frontend/src/pages/settings/avatar-decoration.dialog.vue b/packages/frontend/src/pages/settings/avatar-decoration.dialog.vue
index 7f1c6fd401..853e536ea3 100644
--- a/packages/frontend/src/pages/settings/avatar-decoration.dialog.vue
+++ b/packages/frontend/src/pages/settings/avatar-decoration.dialog.vue
@@ -151,7 +151,7 @@ async function detach() {
 	left: 0;
 	padding: 12px;
 	border-top: solid 0.5px var(--MI_THEME-divider);
-	-webkit-backdrop-filter: var(--blur, blur(15px));
-	backdrop-filter: var(--blur, blur(15px));
+	-webkit-backdrop-filter: var(--MI-blur, blur(15px));
+	backdrop-filter: var(--MI-blur, blur(15px));
 }
 </style>
diff --git a/packages/frontend/src/pages/settings/avatar-decoration.vue b/packages/frontend/src/pages/settings/avatar-decoration.vue
index 77229d3349..9fca306f9f 100644
--- a/packages/frontend/src/pages/settings/avatar-decoration.vue
+++ b/packages/frontend/src/pages/settings/avatar-decoration.vue
@@ -145,7 +145,7 @@ definePageMetadata(() => ({
 
 .current {
 	padding: 16px;
-	border-radius: var(--radius);
+	border-radius: var(--MI-radius);
 }
 
 .decorations {
diff --git a/packages/frontend/src/pages/settings/emoji-picker.vue b/packages/frontend/src/pages/settings/emoji-picker.vue
index 427cdbe64e..fd3581d114 100644
--- a/packages/frontend/src/pages/settings/emoji-picker.vue
+++ b/packages/frontend/src/pages/settings/emoji-picker.vue
@@ -248,8 +248,8 @@ definePageMetadata(() => ({
 
 <style lang="scss" module>
 .tab {
-	margin: calc(var(--margin) / 2) 0;
-	padding: calc(var(--margin) / 2) 0;
+	margin: calc(var(--MI-margin) / 2) 0;
+	padding: calc(var(--MI-margin) / 2) 0;
 	background: var(--MI_THEME-bg);
 }
 
diff --git a/packages/frontend/src/pages/settings/preferences-backups.vue b/packages/frontend/src/pages/settings/preferences-backups.vue
index 80d04ec686..7388e014ed 100644
--- a/packages/frontend/src/pages/settings/preferences-backups.vue
+++ b/packages/frontend/src/pages/settings/preferences-backups.vue
@@ -445,7 +445,7 @@ definePageMetadata(() => ({
 <style lang="scss" module>
 .buttons {
 	display: flex;
-	gap: var(--margin);
+	gap: var(--MI-margin);
 	flex-wrap: wrap;
 }
 
diff --git a/packages/frontend/src/pages/settings/theme.vue b/packages/frontend/src/pages/settings/theme.vue
index 73cc075082..f1ec231588 100644
--- a/packages/frontend/src/pages/settings/theme.vue
+++ b/packages/frontend/src/pages/settings/theme.vue
@@ -412,7 +412,7 @@ definePageMetadata(() => ({
 .rsljpzjq {
 	> .selects {
 		display: flex;
-		gap: 1.5em var(--margin);
+		gap: 1.5em var(--MI-margin);
 		flex-wrap: wrap;
 
 		> .select {
diff --git a/packages/frontend/src/pages/signup-complete.vue b/packages/frontend/src/pages/signup-complete.vue
index ab8502c1e6..14fb96d4f1 100644
--- a/packages/frontend/src/pages/signup-complete.vue
+++ b/packages/frontend/src/pages/signup-complete.vue
@@ -71,7 +71,7 @@ place-content: center;
 .form {
 	position: relative;
 	z-index: 10;
-	border-radius: var(--radius);
+	border-radius: var(--MI-radius);
 	box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1);
 	overflow: clip;
 	max-width: 500px;
diff --git a/packages/frontend/src/pages/tag.vue b/packages/frontend/src/pages/tag.vue
index 3e6d4db03d..b669e25179 100644
--- a/packages/frontend/src/pages/tag.vue
+++ b/packages/frontend/src/pages/tag.vue
@@ -76,8 +76,8 @@ definePageMetadata(() => ({
 
 <style lang="scss" module>
 .footer {
-	-webkit-backdrop-filter: var(--blur, blur(15px));
-	backdrop-filter: var(--blur, blur(15px));
+	-webkit-backdrop-filter: var(--MI-blur, blur(15px));
+	backdrop-filter: var(--MI-blur, blur(15px));
 	background: var(--MI_THEME-acrylicBg);
 	border-top: solid 0.5px var(--MI_THEME-divider);
 	display: flex;
diff --git a/packages/frontend/src/pages/timeline.vue b/packages/frontend/src/pages/timeline.vue
index f913060096..4feba54104 100644
--- a/packages/frontend/src/pages/timeline.vue
+++ b/packages/frontend/src/pages/timeline.vue
@@ -9,10 +9,10 @@ SPDX-License-Identifier: AGPL-3.0-only
 	<MkSpacer :contentMax="800">
 		<MkHorizontalSwipe v-model:tab="src" :tabs="$i ? headerTabs : headerTabsWhenNotLogin">
 			<div :key="src" ref="rootEl">
-				<MkInfo v-if="isBasicTimeline(src) && !defaultStore.reactiveState.timelineTutorials.value[src]" style="margin-bottom: var(--margin);" closable @close="closeTutorial()">
+				<MkInfo v-if="isBasicTimeline(src) && !defaultStore.reactiveState.timelineTutorials.value[src]" style="margin-bottom: var(--MI-margin);" closable @close="closeTutorial()">
 					{{ i18n.ts._timelineDescription[src] }}
 				</MkInfo>
-				<MkPostForm v-if="defaultStore.reactiveState.showFixedPostForm.value" :class="$style.postForm" class="post-form _panel" fixed style="margin-bottom: var(--margin);"/>
+				<MkPostForm v-if="defaultStore.reactiveState.showFixedPostForm.value" :class="$style.postForm" class="post-form _panel" fixed style="margin-bottom: var(--MI-margin);"/>
 				<div v-if="queue > 0" :class="$style.new"><button class="_buttonPrimary" :class="$style.newButton" @click="top()">{{ i18n.ts.newNoteRecived }}</button></div>
 				<div :class="$style.tl">
 					<MkTimeline
@@ -345,30 +345,30 @@ definePageMetadata(() => ({
 <style lang="scss" module>
 .new {
 	position: sticky;
-	top: calc(var(--stickyTop, 0px) + 16px);
+	top: calc(var(--MI-stickyTop, 0px) + 16px);
 	z-index: 1000;
 	width: 100%;
 	margin: calc(-0.675em - 8px) 0;
 
 	&:first-child {
-		margin-top: calc(-0.675em - 8px - var(--margin));
+		margin-top: calc(-0.675em - 8px - var(--MI-margin));
 	}
 }
 
 .newButton {
 	display: block;
-	margin: var(--margin) auto 0 auto;
+	margin: var(--MI-margin) auto 0 auto;
 	padding: 8px 16px;
 	border-radius: 32px;
 }
 
 .postForm {
-	border-radius: var(--radius);
+	border-radius: var(--MI-radius);
 }
 
 .tl {
 	background: var(--MI_THEME-bg);
-	border-radius: var(--radius);
+	border-radius: var(--MI-radius);
 	overflow: clip;
 }
 </style>
diff --git a/packages/frontend/src/pages/user-list-timeline.vue b/packages/frontend/src/pages/user-list-timeline.vue
index a05743a5a1..3efeb46c0a 100644
--- a/packages/frontend/src/pages/user-list-timeline.vue
+++ b/packages/frontend/src/pages/user-list-timeline.vue
@@ -79,26 +79,26 @@ definePageMetadata(() => ({
 <style lang="scss" module>
 .new {
 	position: sticky;
-	top: calc(var(--stickyTop, 0px) + 16px);
+	top: calc(var(--MI-stickyTop, 0px) + 16px);
 	z-index: 1000;
 	width: 100%;
 	margin: calc(-0.675em - 8px) 0;
 
 	&:first-child {
-		margin-top: calc(-0.675em - 8px - var(--margin));
+		margin-top: calc(-0.675em - 8px - var(--MI-margin));
 	}
 }
 
 .newButton {
 	display: block;
-	margin: var(--margin) auto 0 auto;
+	margin: var(--MI-margin) auto 0 auto;
 	padding: 8px 16px;
 	border-radius: 32px;
 }
 
 .tl {
 	background: var(--MI_THEME-bg);
-	border-radius: var(--radius);
+	border-radius: var(--MI-radius);
 	overflow: clip;
 }
 </style>
diff --git a/packages/frontend/src/pages/user/follow-list.vue b/packages/frontend/src/pages/user/follow-list.vue
index e60dccec17..868767e8f4 100644
--- a/packages/frontend/src/pages/user/follow-list.vue
+++ b/packages/frontend/src/pages/user/follow-list.vue
@@ -45,6 +45,6 @@ const followersPagination = {
 .users {
 	display: grid;
 	grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
-	grid-gap: var(--margin);
+	grid-gap: var(--MI-margin);
 }
 </style>
diff --git a/packages/frontend/src/pages/user/gallery.vue b/packages/frontend/src/pages/user/gallery.vue
index 9ba81322ba..0bc5628528 100644
--- a/packages/frontend/src/pages/user/gallery.vue
+++ b/packages/frontend/src/pages/user/gallery.vue
@@ -38,6 +38,6 @@ const pagination = {
 	display: grid;
 	grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
 	grid-gap: 12px;
-	margin: var(--margin);
+	margin: var(--MI-margin);
 }
 </style>
diff --git a/packages/frontend/src/pages/user/home.vue b/packages/frontend/src/pages/user/home.vue
index f0f8724c67..00b5740639 100644
--- a/packages/frontend/src/pages/user/home.vue
+++ b/packages/frontend/src/pages/user/home.vue
@@ -377,8 +377,8 @@ onUnmounted(() => {
 						position: absolute;
 						top: 12px;
 						right: 12px;
-						-webkit-backdrop-filter: var(--blur, blur(8px));
-						backdrop-filter: var(--blur, blur(8px));
+						-webkit-backdrop-filter: var(--MI-blur, blur(8px));
+						backdrop-filter: var(--MI-blur, blur(8px));
 						background: rgba(0, 0, 0, 0.2);
 						padding: 8px;
 						border-radius: 24px;
@@ -432,8 +432,8 @@ onUnmounted(() => {
 							> .add-note-button {
 								background: rgba(0, 0, 0, 0.2);
 								color: #fff;
-								-webkit-backdrop-filter: var(--blur, blur(8px));
-								backdrop-filter: var(--blur, blur(8px));
+								-webkit-backdrop-filter: var(--MI-blur, blur(8px));
+								backdrop-filter: var(--MI-blur, blur(8px));
 								border-radius: 24px;
 								padding: 4px 8px;
 								font-size: 80%;
@@ -616,7 +616,7 @@ onUnmounted(() => {
 
 		> .contents {
 			> .content {
-				margin-bottom: var(--margin);
+				margin-bottom: var(--MI-margin);
 			}
 		}
 	}
@@ -633,7 +633,7 @@ onUnmounted(() => {
 		> .sub {
 			max-width: 350px;
 			min-width: 350px;
-			margin-left: var(--margin);
+			margin-left: var(--MI-margin);
 		}
 	}
 }
@@ -711,7 +711,7 @@ onUnmounted(() => {
 <style lang="scss" module>
 .tl {
 	background: var(--MI_THEME-bg);
-	border-radius: var(--radius);
+	border-radius: var(--MI-radius);
 	overflow: clip;
 }
 
diff --git a/packages/frontend/src/pages/user/index.timeline.vue b/packages/frontend/src/pages/user/index.timeline.vue
index 6339c54ddf..49d015a530 100644
--- a/packages/frontend/src/pages/user/index.timeline.vue
+++ b/packages/frontend/src/pages/user/index.timeline.vue
@@ -51,13 +51,13 @@ const pagination = computed(() => tab.value === 'featured' ? {
 
 <style lang="scss" module>
 .tab {
-	padding: calc(var(--margin) / 2) 0;
+	padding: calc(var(--MI-margin) / 2) 0;
 	background: var(--MI_THEME-bg);
 }
 
 .tl {
 	background: var(--MI_THEME-bg);
-	border-radius: var(--radius);
+	border-radius: var(--MI-radius);
 	overflow: clip;
 }
 </style>
diff --git a/packages/frontend/src/pages/welcome.entrance.a.vue b/packages/frontend/src/pages/welcome.entrance.a.vue
index 8e1f9a4a2c..f0e4a852c9 100644
--- a/packages/frontend/src/pages/welcome.entrance.a.vue
+++ b/packages/frontend/src/pages/welcome.entrance.a.vue
@@ -165,8 +165,8 @@ misskeyApiGet('federation/instances', {
 		right: 0;
 		margin: auto;
 		background: var(--MI_THEME-acrylicPanel);
-		-webkit-backdrop-filter: var(--blur, blur(15px));
-		backdrop-filter: var(--blur, blur(15px));
+		-webkit-backdrop-filter: var(--MI-blur, blur(15px));
+		backdrop-filter: var(--MI-blur, blur(15px));
 		border-radius: 999px;
 		overflow: clip;
 		width: 800px;
diff --git a/packages/frontend/src/pages/welcome.setup.vue b/packages/frontend/src/pages/welcome.setup.vue
index 6174bcd820..33cc139a45 100644
--- a/packages/frontend/src/pages/welcome.setup.vue
+++ b/packages/frontend/src/pages/welcome.setup.vue
@@ -99,7 +99,7 @@ function submit() {
 .form {
 	position: relative;
 	z-index: 10;
-	border-radius: var(--radius);
+	border-radius: var(--MI-radius);
 	box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1);
 	overflow: clip;
 	max-width: 500px;
diff --git a/packages/frontend/src/pages/welcome.timeline.vue b/packages/frontend/src/pages/welcome.timeline.vue
index 732d483615..9be3a80a9e 100644
--- a/packages/frontend/src/pages/welcome.timeline.vue
+++ b/packages/frontend/src/pages/welcome.timeline.vue
@@ -60,7 +60,7 @@ onUpdated(() => {
 		transform: translate3d(0, 0, 0);
 	}
 	100% {
-		transform: translate3d(0, calc(calc(-100% - 128px) - var(--margin)), 0);
+		transform: translate3d(0, calc(calc(-100% - 128px) - var(--MI-margin)), 0);
 	}
 }
 
@@ -69,7 +69,7 @@ onUpdated(() => {
 		transform: translate3d(0, -128px, 0);
 	}
 	100% {
-		transform: translate3d(0, calc(calc(-100% - 128px) - var(--margin)), 0);
+		transform: translate3d(0, calc(calc(-100% - 128px) - var(--MI-margin)), 0);
 	}
 }
 
diff --git a/packages/frontend/src/style.scss b/packages/frontend/src/style.scss
index 424cc02d0e..cfc988bd58 100644
--- a/packages/frontend/src/style.scss
+++ b/packages/frontend/src/style.scss
@@ -7,20 +7,20 @@
  */
 
 :root {
-	--radius: 12px;
-	--marginFull: 16px;
-	--marginHalf: 10px;
+	--MI-radius: 12px;
+	--MI-marginFull: 16px;
+	--MI-marginHalf: 10px;
 
-	--margin: var(--marginFull);
+	--MI-margin: var(--MI-marginFull);
 
 	// switch dynamically
-	--minBottomSpacingMobile: calc(72px + max(12px, env(safe-area-inset-bottom, 0px)));
-	--minBottomSpacing: var(--minBottomSpacingMobile);
+	--MI-minBottomSpacingMobile: calc(72px + max(12px, env(safe-area-inset-bottom, 0px)));
+	--MI-minBottomSpacing: var(--MI-minBottomSpacingMobile);
 
 	//--ad: rgb(255 169 0 / 10%);
 
 	@media (max-width: 500px) {
-		--margin: var(--marginHalf);
+		--MI-margin: var(--MI-marginHalf);
 	}
 }
 
@@ -130,7 +130,7 @@ optgroup, option {
 }
 
 hr {
-	margin: var(--margin) 0 var(--margin) 0;
+	margin: var(--MI-margin) 0 var(--MI-margin) 0;
 	border: none;
 	height: 1px;
 	background: var(--MI_THEME-divider);
@@ -210,8 +210,8 @@ rt {
 	width: 100%;
 	height: 100%;
 	background: var(--MI_THEME-modalBg);
-	-webkit-backdrop-filter: var(--modalBgFilter);
-	backdrop-filter: var(--modalBgFilter);
+	-webkit-backdrop-filter: var(--MI-modalBgFilter);
+	backdrop-filter: var(--MI-modalBgFilter);
 }
 
 ._shadow {
@@ -290,12 +290,12 @@ rt {
 
 ._panel {
 	background: var(--MI_THEME-panel);
-	border-radius: var(--radius);
+	border-radius: var(--MI-radius);
 	overflow: clip;
 }
 
 ._margin {
-	margin: var(--margin) 0;
+	margin: var(--MI-margin) 0;
 }
 
 ._gaps_m {
@@ -313,7 +313,7 @@ rt {
 ._gaps {
 	display: flex;
 	flex-direction: column;
-	gap: var(--margin);
+	gap: var(--MI-margin);
 }
 
 ._buttons {
@@ -336,7 +336,7 @@ rt {
 	box-sizing: border-box;
 	text-align: center;
 	border: solid 0.5px var(--MI_THEME-divider);
-	border-radius: var(--radius);
+	border-radius: var(--MI-radius);
 
 	&:active {
 		border-color: var(--MI_THEME-accent);
@@ -345,14 +345,14 @@ rt {
 
 ._popup {
 	background: var(--MI_THEME-popup);
-	border-radius: var(--radius);
+	border-radius: var(--MI-radius);
 	contain: content;
 }
 
 ._acrylic {
 	background: var(--MI_THEME-acrylicPanel);
-	-webkit-backdrop-filter: var(--blur, blur(15px));
-	backdrop-filter: var(--blur, blur(15px));
+	-webkit-backdrop-filter: var(--MI-blur, blur(15px));
+	backdrop-filter: var(--MI-blur, blur(15px));
 }
 
 ._formLinksGrid {
diff --git a/packages/frontend/src/ui/_common_/common.vue b/packages/frontend/src/ui/_common_/common.vue
index e3c0f1f4ce..d145b9b6c6 100644
--- a/packages/frontend/src/ui/_common_/common.vue
+++ b/packages/frontend/src/ui/_common_/common.vue
@@ -116,27 +116,27 @@ if ($i) {
 .notifications {
 	position: fixed;
 	z-index: 3900000;
-	padding: 0 var(--margin);
+	padding: 0 var(--MI-margin);
 	pointer-events: none;
 	display: flex;
 
 	&.notificationsPosition_leftTop {
-		top: var(--margin);
+		top: var(--MI-margin);
 		left: 0;
 	}
 
 	&.notificationsPosition_rightTop {
-		top: var(--margin);
+		top: var(--MI-margin);
 		right: 0;
 	}
 
 	&.notificationsPosition_leftBottom {
-		bottom: calc(var(--minBottomSpacing) + var(--margin));
+		bottom: calc(var(--MI-minBottomSpacing) + var(--MI-margin));
 		left: 0;
 	}
 
 	&.notificationsPosition_rightBottom {
-		bottom: calc(var(--minBottomSpacing) + var(--margin));
+		bottom: calc(var(--MI-minBottomSpacing) + var(--MI-margin));
 		right: 0;
 	}
 
diff --git a/packages/frontend/src/ui/_common_/navbar-for-mobile.vue b/packages/frontend/src/ui/_common_/navbar-for-mobile.vue
index a71f57670d..9acf7b2ede 100644
--- a/packages/frontend/src/ui/_common_/navbar-for-mobile.vue
+++ b/packages/frontend/src/ui/_common_/navbar-for-mobile.vue
@@ -94,8 +94,8 @@ function more() {
 	z-index: 1;
 	padding: 20px 0;
 	background: var(--nav-bg-transparent);
-	-webkit-backdrop-filter: var(--blur, blur(8px));
-	backdrop-filter: var(--blur, blur(8px));
+	-webkit-backdrop-filter: var(--MI-blur, blur(8px));
+	backdrop-filter: var(--MI-blur, blur(8px));
 }
 
 .banner {
@@ -128,8 +128,8 @@ function more() {
 	bottom: 0;
 	padding: 20px 0;
 	background: var(--nav-bg-transparent);
-	-webkit-backdrop-filter: var(--blur, blur(8px));
-	backdrop-filter: var(--blur, blur(8px));
+	-webkit-backdrop-filter: var(--MI-blur, blur(8px));
+	backdrop-filter: var(--MI-blur, blur(8px));
 }
 
 .post {
diff --git a/packages/frontend/src/ui/_common_/navbar.vue b/packages/frontend/src/ui/_common_/navbar.vue
index 4d01330432..cbfdaac235 100644
--- a/packages/frontend/src/ui/_common_/navbar.vue
+++ b/packages/frontend/src/ui/_common_/navbar.vue
@@ -146,8 +146,8 @@ function more(ev: MouseEvent) {
 		z-index: 1;
 		padding: 20px 0;
 		background: var(--nav-bg-transparent);
-		-webkit-backdrop-filter: var(--blur, blur(8px));
-		backdrop-filter: var(--blur, blur(8px));
+		-webkit-backdrop-filter: var(--MI-blur, blur(8px));
+		backdrop-filter: var(--MI-blur, blur(8px));
 	}
 
 	.banner {
@@ -189,8 +189,8 @@ function more(ev: MouseEvent) {
 		bottom: 0;
 		padding-top: 20px;
 		background: var(--nav-bg-transparent);
-		-webkit-backdrop-filter: var(--blur, blur(8px));
-		backdrop-filter: var(--blur, blur(8px));
+		-webkit-backdrop-filter: var(--MI-blur, blur(8px));
+		backdrop-filter: var(--MI-blur, blur(8px));
 	}
 
 	.post {
@@ -380,8 +380,8 @@ function more(ev: MouseEvent) {
 		z-index: 1;
 		padding: 20px 0;
 		background: var(--nav-bg-transparent);
-		-webkit-backdrop-filter: var(--blur, blur(8px));
-		backdrop-filter: var(--blur, blur(8px));
+		-webkit-backdrop-filter: var(--MI-blur, blur(8px));
+		backdrop-filter: var(--MI-blur, blur(8px));
 	}
 
 	.instance {
@@ -410,8 +410,8 @@ function more(ev: MouseEvent) {
 		bottom: 0;
 		padding-top: 20px;
 		background: var(--nav-bg-transparent);
-		-webkit-backdrop-filter: var(--blur, blur(8px));
-		backdrop-filter: var(--blur, blur(8px));
+		-webkit-backdrop-filter: var(--MI-blur, blur(8px));
+		backdrop-filter: var(--MI-blur, blur(8px));
 	}
 
 	.post {
diff --git a/packages/frontend/src/ui/_common_/stream-indicator.vue b/packages/frontend/src/ui/_common_/stream-indicator.vue
index ad93b7e61c..cc62a28b14 100644
--- a/packages/frontend/src/ui/_common_/stream-indicator.vue
+++ b/packages/frontend/src/ui/_common_/stream-indicator.vue
@@ -48,8 +48,8 @@ onUnmounted(() => {
 .root {
 	position: fixed;
 	z-index: v-bind(zIndex);
-	bottom: calc(var(--minBottomSpacing) + var(--margin));
-	right: var(--margin);
+	bottom: calc(var(--MI-minBottomSpacing) + var(--MI-margin));
+	right: var(--MI-margin);
 	margin: 0;
 	padding: 12px;
 	font-size: 0.9em;
diff --git a/packages/frontend/src/ui/classic.vue b/packages/frontend/src/ui/classic.vue
index 9715e1ba18..5ea9bf7068 100644
--- a/packages/frontend/src/ui/classic.vue
+++ b/packages/frontend/src/ui/classic.vue
@@ -12,7 +12,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 			<XSidebar/>
 		</div>
 		<div v-else-if="!pageMetadata?.needWideArea" ref="widgetsLeft" class="widgets left">
-			<XWidgets place="left" :marginTop="'var(--margin)'" @mounted="attachSticky(widgetsLeft)"/>
+			<XWidgets place="left" :marginTop="'var(--MI-margin)'" @mounted="attachSticky(widgetsLeft)"/>
 		</div>
 
 		<main class="main" @contextmenu.stop="onContextmenu">
@@ -22,7 +22,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 		</main>
 
 		<div v-if="isDesktop && !pageMetadata?.needWideArea" ref="widgetsRight" class="widgets right">
-			<XWidgets :place="showMenuOnTop ? 'right' : null" :marginTop="showMenuOnTop ? '0' : 'var(--margin)'" @mounted="attachSticky(widgetsRight)"/>
+			<XWidgets :place="showMenuOnTop ? 'right' : null" :marginTop="showMenuOnTop ? '0' : 'var(--MI-margin)'" @mounted="attachSticky(widgetsRight)"/>
 		</div>
 	</div>
 
@@ -217,7 +217,7 @@ onMounted(() => {
 
 	&.wallpaper {
 		background: var(--MI_THEME-wallpaperOverlay);
-		//backdrop-filter: var(--blur, blur(4px));
+		//backdrop-filter: var(--MI-blur, blur(4px));
 	}
 
 	> .columns {
@@ -253,13 +253,13 @@ onMounted(() => {
 			border-right: solid 1px var(--MI_THEME-divider);
 			border-radius: 0;
 			overflow: clip;
-			--margin: 12px;
+			--MI-margin: 12px;
 		}
 
 		> .widgets {
 			//--MI_THEME-panelBorder: none;
 			width: 300px;
-			padding-bottom: calc(var(--margin) + env(safe-area-inset-bottom, 0px));
+			padding-bottom: calc(var(--MI-margin) + env(safe-area-inset-bottom, 0px));
 
 			@media (max-width: $widgets-hide-threshold) {
 				display: none;
@@ -278,12 +278,12 @@ onMounted(() => {
 			> .main {
 				margin-top: 0;
 				border: solid 1px var(--MI_THEME-divider);
-				border-radius: var(--radius);
-				--stickyTop: var(--globalHeaderHeight);
+				border-radius: var(--MI-radius);
+				--MI-stickyTop: var(--globalHeaderHeight);
 			}
 
 			> .widgets {
-				--stickyTop: var(--globalHeaderHeight);
+				--MI-stickyTop: var(--globalHeaderHeight);
 				margin-top: 0;
 			}
 		}
@@ -314,7 +314,7 @@ onMounted(() => {
 		right: 0;
 		z-index: 1001;
 		height: 100dvh;
-		padding: var(--margin) var(--margin) calc(var(--margin) + env(safe-area-inset-bottom, 0px));
+		padding: var(--MI-margin) var(--MI-margin) calc(var(--MI-margin) + env(safe-area-inset-bottom, 0px));
 		box-sizing: border-box;
 		overflow: auto;
 		background: var(--MI_THEME-bg);
diff --git a/packages/frontend/src/ui/deck.vue b/packages/frontend/src/ui/deck.vue
index 623a109e88..36ffca8264 100644
--- a/packages/frontend/src/ui/deck.vue
+++ b/packages/frontend/src/ui/deck.vue
@@ -305,7 +305,7 @@ body {
 .root {
 	$nav-hide-threshold: 650px; // TODO: どこかに集約したい
 
-	--margin: var(--marginHalf);
+	--MI-margin: var(--MI-marginHalf);
 
 	--columnGap: 6px;
 
@@ -428,8 +428,8 @@ body {
 	grid-gap: 8px;
 	width: 100%;
 	box-sizing: border-box;
-	-webkit-backdrop-filter: var(--blur, blur(32px));
-	backdrop-filter: var(--blur, blur(32px));
+	-webkit-backdrop-filter: var(--MI-blur, blur(32px));
+	backdrop-filter: var(--MI-blur, blur(32px));
 	background-color: var(--MI_THEME-header);
 	border-top: solid 0.5px var(--MI_THEME-divider);
 }
diff --git a/packages/frontend/src/ui/deck/column.vue b/packages/frontend/src/ui/deck/column.vue
index 4aaaea0fd9..da0bf24a56 100644
--- a/packages/frontend/src/ui/deck/column.vue
+++ b/packages/frontend/src/ui/deck/column.vue
@@ -332,8 +332,8 @@ function onDrop(ev) {
 
 	&.naked {
 		background: var(--MI_THEME-acrylicBg) !important;
-		-webkit-backdrop-filter: var(--blur, blur(10px));
-		backdrop-filter: var(--blur, blur(10px));
+		-webkit-backdrop-filter: var(--MI-blur, blur(10px));
+		backdrop-filter: var(--MI-blur, blur(10px));
 
 		> .header {
 			background: transparent;
diff --git a/packages/frontend/src/ui/deck/widgets-column.vue b/packages/frontend/src/ui/deck/widgets-column.vue
index da12570ae2..a0e62c8264 100644
--- a/packages/frontend/src/ui/deck/widgets-column.vue
+++ b/packages/frontend/src/ui/deck/widgets-column.vue
@@ -57,10 +57,10 @@ const menu = [{
 
 <style lang="scss" module>
 .root {
-	--margin: 8px;
+	--MI-margin: 8px;
 	--MI_THEME-panelBorder: none;
 
-	padding: 0 var(--margin);
+	padding: 0 var(--MI-margin);
 }
 
 .intro {
diff --git a/packages/frontend/src/ui/universal.vue b/packages/frontend/src/ui/universal.vue
index 73c4e7c195..9fc8bd102d 100644
--- a/packages/frontend/src/ui/universal.vue
+++ b/packages/frontend/src/ui/universal.vue
@@ -225,12 +225,12 @@ provide<Ref<number>>(CURRENT_STICKY_BOTTOM, navFooterHeight);
 watch(navFooter, () => {
 	if (navFooter.value) {
 		navFooterHeight.value = navFooter.value.offsetHeight;
-		document.body.style.setProperty('--stickyBottom', `${navFooterHeight.value}px`);
-		document.body.style.setProperty('--minBottomSpacing', 'var(--minBottomSpacingMobile)');
+		document.body.style.setProperty('--MI-stickyBottom', `${navFooterHeight.value}px`);
+		document.body.style.setProperty('--MI-minBottomSpacing', 'var(--MI-minBottomSpacingMobile)');
 	} else {
 		navFooterHeight.value = 0;
-		document.body.style.setProperty('--stickyBottom', '0px');
-		document.body.style.setProperty('--minBottomSpacing', '0px');
+		document.body.style.setProperty('--MI-stickyBottom', '0px');
+		document.body.style.setProperty('--MI-minBottomSpacing', '0px');
 	}
 }, {
 	immediate: true,
@@ -336,7 +336,7 @@ $widgets-hide-threshold: 1090px;
 	height: 100%;
 	box-sizing: border-box;
 	overflow: auto;
-	padding: var(--margin) var(--margin) calc(var(--margin) + env(safe-area-inset-bottom, 0px));
+	padding: var(--MI-margin) var(--MI-margin) calc(var(--MI-margin) + env(safe-area-inset-bottom, 0px));
 	border-left: solid 0.5px var(--MI_THEME-divider);
 	background: var(--MI_THEME-bg);
 
@@ -370,7 +370,7 @@ $widgets-hide-threshold: 1090px;
 	z-index: 1001;
 	width: 310px;
 	height: 100dvh;
-	padding: var(--margin) var(--margin) calc(var(--margin) + env(safe-area-inset-bottom, 0px)) !important;
+	padding: var(--MI-margin) var(--MI-margin) calc(var(--MI-margin) + env(safe-area-inset-bottom, 0px)) !important;
 	box-sizing: border-box;
 	overflow: auto;
 	overscroll-behavior: contain;
@@ -400,8 +400,8 @@ $widgets-hide-threshold: 1090px;
 	grid-gap: 8px;
 	width: 100%;
 	box-sizing: border-box;
-	-webkit-backdrop-filter: var(--blur, blur(24px));
-	backdrop-filter: var(--blur, blur(24px));
+	-webkit-backdrop-filter: var(--MI-blur, blur(24px));
+	backdrop-filter: var(--MI-blur, blur(24px));
 	background-color: var(--MI_THEME-header);
 	border-top: solid 0.5px var(--MI_THEME-divider);
 }
@@ -484,6 +484,6 @@ $widgets-hide-threshold: 1090px;
 }
 
 .spacer {
-	height: calc(var(--minBottomSpacing));
+	height: calc(var(--MI-minBottomSpacing));
 }
 </style>
diff --git a/packages/frontend/src/ui/zen.vue b/packages/frontend/src/ui/zen.vue
index 93d57b647e..1f73b5fcaf 100644
--- a/packages/frontend/src/ui/zen.vue
+++ b/packages/frontend/src/ui/zen.vue
@@ -63,12 +63,12 @@ document.documentElement.style.overflowY = 'scroll';
 }
 
 .rootWithBottom {
-	min-height: calc(100dvh - (60px + (var(--margin) * 2) + env(safe-area-inset-bottom, 0px)));
+	min-height: calc(100dvh - (60px + (var(--MI-margin) * 2) + env(safe-area-inset-bottom, 0px)));
 	box-sizing: border-box;
 }
 
 .bottom {
-	height: calc(60px + (var(--margin) * 2) + env(safe-area-inset-bottom, 0px));
+	height: calc(60px + (var(--MI-margin) * 2) + env(safe-area-inset-bottom, 0px));
 	width: 100%;
 	margin-top: auto;
 }
@@ -83,7 +83,7 @@ document.documentElement.style.overflowY = 'scroll';
 	border-radius: 100%;
 	background: var(--MI_THEME-panel);
 	color: var(--MI_THEME-fg);
-	right: var(--margin);
-	bottom: calc(var(--margin) + env(safe-area-inset-bottom, 0px));
+	right: var(--MI-margin);
+	bottom: calc(var(--MI-margin) + env(safe-area-inset-bottom, 0px));
 }
 </style>
diff --git a/packages/frontend/src/widgets/WidgetBirthdayFollowings.vue b/packages/frontend/src/widgets/WidgetBirthdayFollowings.vue
index bcfaaf00ab..c2bda85ac7 100644
--- a/packages/frontend/src/widgets/WidgetBirthdayFollowings.vue
+++ b/packages/frontend/src/widgets/WidgetBirthdayFollowings.vue
@@ -115,7 +115,7 @@ defineExpose<WidgetComponentExpose>({
 <style lang="scss" module>
 .bdayFRoot {
 	overflow: hidden;
-	min-height: calc(calc(calc(50px * 3) - 8px) + calc(var(--margin) * 2));
+	min-height: calc(calc(calc(50px * 3) - 8px) + calc(var(--MI-margin) * 2));
 }
 .bdayFGrid {
 	display: grid;
@@ -123,7 +123,7 @@ defineExpose<WidgetComponentExpose>({
 	grid-template-rows: repeat(3, 42px);
 	place-content: center;
 	gap: 8px;
-	margin: var(--margin) auto;
+	margin: var(--MI-margin) auto;
 }
 
 .bdayFFallback {
@@ -139,6 +139,6 @@ defineExpose<WidgetComponentExpose>({
 	width: auto;
 	max-width: 90%;
 	margin-bottom: 8px;
-	border-radius: var(--radius);
+	border-radius: var(--MI-radius);
 }
 </style>

From 54849bde6c349808f563d9a3165a27397cc9b71d Mon Sep 17 00:00:00 2001
From: syuilo <4439005+syuilo@users.noreply.github.com>
Date: Thu, 10 Oct 2024 16:14:11 +0900
Subject: [PATCH 076/121] clean up

---
 packages/frontend/src/components/MkDrive.folder.vue | 4 +---
 packages/frontend/src/components/MkFlashPreview.vue | 1 -
 packages/frontend/src/components/MkMediaBanner.vue  | 1 -
 packages/frontend/src/components/MkPagePreview.vue  | 1 -
 packages/frontend/src/components/MkUrlPreview.vue   | 5 ++---
 5 files changed, 3 insertions(+), 9 deletions(-)

diff --git a/packages/frontend/src/components/MkDrive.folder.vue b/packages/frontend/src/components/MkDrive.folder.vue
index 391acbc8d3..44e3b59ade 100644
--- a/packages/frontend/src/components/MkDrive.folder.vue
+++ b/packages/frontend/src/components/MkDrive.folder.vue
@@ -36,13 +36,13 @@ SPDX-License-Identifier: AGPL-3.0-only
 <script lang="ts" setup>
 import { computed, defineAsyncComponent, ref } from 'vue';
 import * as Misskey from 'misskey-js';
+import type { MenuItem } from '@/types/menu.js';
 import * as os from '@/os.js';
 import { misskeyApi } from '@/scripts/misskey-api.js';
 import { i18n } from '@/i18n.js';
 import { defaultStore } from '@/store.js';
 import { claimAchievement } from '@/scripts/achievements.js';
 import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
-import type { MenuItem } from '@/types/menu.js';
 
 const props = withDefaults(defineProps<{
 	folder: Misskey.entities.DriveFolder;
@@ -375,7 +375,6 @@ function onContextmenu(ev: MouseEvent) {
 .name {
 	margin: 0;
 	font-size: 0.9em;
-	color: var(--desktopDriveFolderFg);
 }
 
 .icon {
@@ -388,6 +387,5 @@ function onContextmenu(ev: MouseEvent) {
 	margin: 4px 4px;
 	font-size: 0.8em;
 	text-align: right;
-	color: var(--desktopDriveFolderFg);
 }
 </style>
diff --git a/packages/frontend/src/components/MkFlashPreview.vue b/packages/frontend/src/components/MkFlashPreview.vue
index 589dd1ce82..b7278ac742 100644
--- a/packages/frontend/src/components/MkFlashPreview.vue
+++ b/packages/frontend/src/components/MkFlashPreview.vue
@@ -83,7 +83,6 @@ const props = defineProps<{
 			> p {
 				display: inline-block;
 				margin: 0;
-				color: var(--urlPreviewInfo);
 				font-size: 0.8em;
 				line-height: 16px;
 				vertical-align: top;
diff --git a/packages/frontend/src/components/MkMediaBanner.vue b/packages/frontend/src/components/MkMediaBanner.vue
index 11995e1f3b..3e521e0a03 100644
--- a/packages/frontend/src/components/MkMediaBanner.vue
+++ b/packages/frontend/src/components/MkMediaBanner.vue
@@ -68,7 +68,6 @@ async function show() {
 }
 
 .download {
-	background: var(--noteAttachedFile);
 }
 
 .sensitive {
diff --git a/packages/frontend/src/components/MkPagePreview.vue b/packages/frontend/src/components/MkPagePreview.vue
index 19579cc4cc..35a37a1f7d 100644
--- a/packages/frontend/src/components/MkPagePreview.vue
+++ b/packages/frontend/src/components/MkPagePreview.vue
@@ -115,7 +115,6 @@ const props = defineProps<{
 			> p {
 				display: inline-block;
 				margin: 0;
-				color: var(--urlPreviewInfo);
 				font-size: 0.8em;
 				line-height: 16px;
 				vertical-align: top;
diff --git a/packages/frontend/src/components/MkUrlPreview.vue b/packages/frontend/src/components/MkUrlPreview.vue
index f38e31c894..c287effadc 100644
--- a/packages/frontend/src/components/MkUrlPreview.vue
+++ b/packages/frontend/src/components/MkUrlPreview.vue
@@ -84,13 +84,13 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <script lang="ts" setup>
 import { defineAsyncComponent, onDeactivated, onUnmounted, ref } from 'vue';
-import type { summaly } from '@misskey-dev/summaly';
 import { url as local } from '@@/js/config.js';
+import { versatileLang } from '@@/js/intl-const.js';
+import type { summaly } from '@misskey-dev/summaly';
 import { i18n } from '@/i18n.js';
 import * as os from '@/os.js';
 import { deviceKind } from '@/scripts/device-kind.js';
 import MkButton from '@/components/MkButton.vue';
-import { versatileLang } from '@@/js/intl-const.js';
 import { transformPlayerUrl } from '@/scripts/player-url-transform.js';
 import { defaultStore } from '@/store.js';
 
@@ -317,7 +317,6 @@ onUnmounted(() => {
 .siteName {
 	display: inline-block;
 	margin: 0;
-	color: var(--urlPreviewInfo);
 	font-size: 0.8em;
 	line-height: 16px;
 	vertical-align: top;

From 4c84842f3d8f969925f6202a682c0c4e0168d804 Mon Sep 17 00:00:00 2001
From: syuilo <4439005+syuilo@users.noreply.github.com>
Date: Thu, 10 Oct 2024 16:14:32 +0900
Subject: [PATCH 077/121] :art:

---
 packages/frontend/src/components/global/MkAd.vue | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/packages/frontend/src/components/global/MkAd.vue b/packages/frontend/src/components/global/MkAd.vue
index b525a81fbe..646304fb06 100644
--- a/packages/frontend/src/components/global/MkAd.vue
+++ b/packages/frontend/src/components/global/MkAd.vue
@@ -43,9 +43,9 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <script lang="ts" setup>
 import { ref, computed } from 'vue';
+import { url as local, host } from '@@/js/config.js';
 import { i18n } from '@/i18n.js';
 import { instance } from '@/instance.js';
-import { url as local, host } from '@@/js/config.js';
 import MkButton from '@/components/MkButton.vue';
 import { defaultStore } from '@/store.js';
 import * as os from '@/os.js';
@@ -124,7 +124,7 @@ function reduceFrequency(): void {
 <style lang="scss" module>
 .root {
 	background-size: auto auto;
-	background-image: repeating-linear-gradient(45deg, transparent, transparent 8px, var(--ad) 8px, var(--ad) 14px );
+	background-image: repeating-linear-gradient(45deg, transparent, transparent 8px, var(--bg) 8px, var(--bg) 14px );
 }
 
 .main {

From 67a5fccb3b49b07fff624114e29051b3b0825f5e Mon Sep 17 00:00:00 2001
From: syuilo <4439005+syuilo@users.noreply.github.com>
Date: Thu, 10 Oct 2024 16:16:47 +0900
Subject: [PATCH 078/121] Update CHANGELOG.md

---
 CHANGELOG.md | 10 ++--------
 1 file changed, 2 insertions(+), 8 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0010b1fa5d..300dfedd02 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,14 +1,8 @@
 ## 2024.10.1
 
-### General
--
-
 ### Client
-- メールアドレス不要でCaptchaが有効な場合にアカウント登録完了後自動でのログインに失敗する問題を修正
-
-### Server
--
-
+- Fix: メールアドレス不要でCaptchaが有効な場合にアカウント登録完了後自動でのログインに失敗する問題を修正
+- Enhance: l10nの更新
 
 ## 2024.10.0
 

From 132c4ba6cef1524de5430f66e931cab9ca3010f4 Mon Sep 17 00:00:00 2001
From: "github-actions[bot]" <github-actions[bot]@users.noreply.github.com>
Date: Thu, 10 Oct 2024 07:24:24 +0000
Subject: [PATCH 079/121] Bump version to 2024.10.1-beta.2

---
 package.json                     | 2 +-
 packages/misskey-js/package.json | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/package.json b/package.json
index 7fc5d05a3f..31d46d79f8 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
 	"name": "misskey",
-	"version": "2024.10.1-beta.1",
+	"version": "2024.10.1-beta.2",
 	"codename": "nasubi",
 	"repository": {
 		"type": "git",
diff --git a/packages/misskey-js/package.json b/packages/misskey-js/package.json
index 0fea7a4d43..268eab7978 100644
--- a/packages/misskey-js/package.json
+++ b/packages/misskey-js/package.json
@@ -1,7 +1,7 @@
 {
 	"type": "module",
 	"name": "misskey-js",
-	"version": "2024.10.1-beta.1",
+	"version": "2024.10.1-beta.2",
 	"description": "Misskey SDK for JavaScript",
 	"license": "MIT",
 	"main": "./built/index.js",

From 1ad31485334a310ddd1c5a550b56695e3c183ab1 Mon Sep 17 00:00:00 2001
From: syuilo <4439005+syuilo@users.noreply.github.com>
Date: Thu, 10 Oct 2024 17:35:10 +0900
Subject: [PATCH 080/121] clean up

---
 packages/frontend/src/style.scss | 2 --
 1 file changed, 2 deletions(-)

diff --git a/packages/frontend/src/style.scss b/packages/frontend/src/style.scss
index cfc988bd58..1e6561bdb9 100644
--- a/packages/frontend/src/style.scss
+++ b/packages/frontend/src/style.scss
@@ -17,8 +17,6 @@
 	--MI-minBottomSpacingMobile: calc(72px + max(12px, env(safe-area-inset-bottom, 0px)));
 	--MI-minBottomSpacing: var(--MI-minBottomSpacingMobile);
 
-	//--ad: rgb(255 169 0 / 10%);
-
 	@media (max-width: 500px) {
 		--MI-margin: var(--MI-marginHalf);
 	}

From d376aab45edc2170592a256a429a1d0b364bd7f5 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=8B=E3=81=A3=E3=81=93=E3=81=8B=E3=82=8A?=
 <67428053+kakkokari-gtyih@users.noreply.github.com>
Date: Thu, 10 Oct 2024 17:39:20 +0900
Subject: [PATCH 081/121] =?UTF-8?q?Update=20CHANGELOG.md=20(=E6=9B=B8?=
 =?UTF-8?q?=E3=81=8D=E6=96=B9=E3=82=92=E6=8F=83=E3=81=88=E3=82=8B)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

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

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 300dfedd02..ee37bb3c6f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,8 +1,8 @@
 ## 2024.10.1
 
 ### Client
-- Fix: メールアドレス不要でCaptchaが有効な場合にアカウント登録完了後自動でのログインに失敗する問題を修正
 - Enhance: l10nの更新
+- Fix: メールアドレス不要でCaptchaが有効な場合にアカウント登録完了後自動でのログインに失敗する問題を修正
 
 ## 2024.10.0
 

From 12bc671511f301598d57635a7125157b963a1973 Mon Sep 17 00:00:00 2001
From: FineArchs <133759614+FineArchs@users.noreply.github.com>
Date: Fri, 11 Oct 2024 17:17:45 +0900
Subject: [PATCH 082/121] =?UTF-8?q?fix:=20admin/emoji/update=20=E3=81=A7?=
 =?UTF-8?q?=E4=B8=8D=E6=AD=A3=E3=81=AA=E3=82=A8=E3=83=A9=E3=83=BC=E3=81=8C?=
 =?UTF-8?q?=E7=99=BA=E7=94=9F=E3=81=99=E3=82=8B=20(#14750)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

* fix emoji updating bug

* update changelog

* type fix

* " -> '

* conprehensiveness check

* lint

* undefined -> null
---
 CHANGELOG.md                                  |  3 ++
 .../backend/src/core/CustomEmojiService.ts    | 29 ++++++++++++----
 .../api/endpoints/admin/emoji/update.ts       | 33 +++++++++----------
 3 files changed, 40 insertions(+), 25 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index ee37bb3c6f..b449a1b91e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,9 @@
 - Enhance: l10nの更新
 - Fix: メールアドレス不要でCaptchaが有効な場合にアカウント登録完了後自動でのログインに失敗する問題を修正
 
+### Server
+- Fix: `admin/emoji/update`エンドポイントのidのみ指定した時不正なエラーが発生するバグを修正
+
 ## 2024.10.0
 
 ### Note
diff --git a/packages/backend/src/core/CustomEmojiService.ts b/packages/backend/src/core/CustomEmojiService.ts
index 5db3c5b980..4566113449 100644
--- a/packages/backend/src/core/CustomEmojiService.ts
+++ b/packages/backend/src/core/CustomEmojiService.ts
@@ -103,19 +103,33 @@ export class CustomEmojiService implements OnApplicationShutdown {
 	}
 
 	@bindThis
-	public async update(id: MiEmoji['id'], data: {
+	public async update(data: (
+		{ id: MiEmoji['id'], name?: string; } | { name: string; id?: MiEmoji['id'], }
+	) & {
 		driveFile?: MiDriveFile;
-		name?: string;
 		category?: string | null;
 		aliases?: string[];
 		license?: string | null;
 		isSensitive?: boolean;
 		localOnly?: boolean;
 		roleIdsThatCanBeUsedThisEmojiAsReaction?: MiRole['id'][];
-	}, moderator?: MiUser): Promise<void> {
-		const emoji = await this.emojisRepository.findOneByOrFail({ id: id });
-		const sameNameEmoji = await this.emojisRepository.findOneBy({ name: data.name, host: IsNull() });
-		if (sameNameEmoji != null && sameNameEmoji.id !== id) throw new Error('name already exists');
+	}, moderator?: MiUser): Promise<
+		null
+		| 'NO_SUCH_EMOJI'
+		| 'SAME_NAME_EMOJI_EXISTS'
+	> {
+		const emoji = data.id
+			? await this.getEmojiById(data.id)
+			: await this.getEmojiByName(data.name!);
+		if (emoji === null) return 'NO_SUCH_EMOJI';
+		const id = emoji.id;
+
+		// IDと絵文字名が両方指定されている場合は絵文字名の変更を行うため重複チェックが必要
+		const doNameUpdate = data.id && data.name && (data.name !== emoji.name);
+		if (doNameUpdate) {
+			const isDuplicate = await this.checkDuplicate(data.name!);
+			if (isDuplicate) return 'SAME_NAME_EMOJI_EXISTS';
+		}
 
 		await this.emojisRepository.update(emoji.id, {
 			updatedAt: new Date(),
@@ -135,7 +149,7 @@ export class CustomEmojiService implements OnApplicationShutdown {
 
 		const packed = await this.emojiEntityService.packDetailed(emoji.id);
 
-		if (emoji.name === data.name) {
+		if (!doNameUpdate) {
 			this.globalEventService.publishBroadcastStream('emojiUpdated', {
 				emojis: [packed],
 			});
@@ -157,6 +171,7 @@ export class CustomEmojiService implements OnApplicationShutdown {
 				after: updated,
 			});
 		}
+		return null;
 	}
 
 	@bindThis
diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/update.ts b/packages/backend/src/server/api/endpoints/admin/emoji/update.ts
index 22609a16a3..212cba5c5d 100644
--- a/packages/backend/src/server/api/endpoints/admin/emoji/update.ts
+++ b/packages/backend/src/server/api/endpoints/admin/emoji/update.ts
@@ -6,7 +6,7 @@
 import { Inject, Injectable } from '@nestjs/common';
 import { Endpoint } from '@/server/api/endpoint-base.js';
 import { CustomEmojiService } from '@/core/CustomEmojiService.js';
-import type { DriveFilesRepository } from '@/models/_.js';
+import type { DriveFilesRepository, MiEmoji } from '@/models/_.js';
 import { DI } from '@/di-symbols.js';
 import { ApiError } from '../../../error.js';
 
@@ -78,25 +78,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 				if (driveFile == null) throw new ApiError(meta.errors.noSuchFile);
 			}
 
-			let emojiId;
-			if (ps.id) {
-				emojiId = ps.id;
-				const emoji = await this.customEmojiService.getEmojiById(ps.id);
-				if (!emoji) throw new ApiError(meta.errors.noSuchEmoji);
-				if (ps.name && (ps.name !== emoji.name)) {
-					const isDuplicate = await this.customEmojiService.checkDuplicate(ps.name);
-					if (isDuplicate) throw new ApiError(meta.errors.sameNameEmojiExists);
-				}
-			} else {
-				if (!ps.name) throw new Error('Invalid Params unexpectedly passed. This is a BUG. Please report it to the development team.');
-				const emoji = await this.customEmojiService.getEmojiByName(ps.name);
-				if (!emoji) throw new ApiError(meta.errors.noSuchEmoji);
-				emojiId = emoji.id;
-			}
+			// JSON schemeのanyOfの型変換がうまくいっていないらしい
+			const required = { id: ps.id, name: ps.name } as 
+				| { id: MiEmoji['id']; name?: string }
+				| { id?: MiEmoji['id']; name: string };
 
-			await this.customEmojiService.update(emojiId, {
+			const error = await this.customEmojiService.update({
+				...required,
 				driveFile,
-				name: ps.name,
 				category: ps.category,
 				aliases: ps.aliases,
 				license: ps.license,
@@ -104,6 +93,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 				localOnly: ps.localOnly,
 				roleIdsThatCanBeUsedThisEmojiAsReaction: ps.roleIdsThatCanBeUsedThisEmojiAsReaction,
 			}, me);
+
+			switch (error) {
+				case null: return;
+				case 'NO_SUCH_EMOJI': throw new ApiError(meta.errors.noSuchEmoji);
+				case 'SAME_NAME_EMOJI_EXISTS': throw new ApiError(meta.errors.sameNameEmojiExists);
+			}
+			// 網羅性チェック
+			const mustBeNever: never = error;
 		});
 	}
 }

From a2cd6a7709ffacfabb738deac22cb0fd1eb7d493 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, 11 Oct 2024 20:59:36 +0900
Subject: [PATCH 083/121] =?UTF-8?q?feat(backend):=207=E6=97=A5=E9=96=93?=
 =?UTF-8?q?=E9=81=8B=E5=96=B6=E3=81=AE=E3=82=A2=E3=82=AF=E3=83=86=E3=82=A3?=
 =?UTF-8?q?=E3=83=93=E3=83=86=E3=82=A3=E3=81=8C=E3=81=AA=E3=81=84=E3=82=B5?=
 =?UTF-8?q?=E3=83=BC=E3=83=90=E3=82=92=E8=87=AA=E5=8B=95=E7=9A=84=E3=81=AB?=
 =?UTF-8?q?=E6=8B=9B=E5=BE=85=E5=88=B6=E3=81=AB=E3=81=99=E3=82=8B=20(#1474?=
 =?UTF-8?q?6)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

* feat(backend): 7日間運営のアクティビティがないサーバを自動的に招待制にする

* fix RoleService.

* fix

* fix

* fix

* add test and fix

* fix

* fix CHANGELOG.md

* fix test
---
 CHANGELOG.md                                  |   5 +
 .../core/AbuseReportNotificationService.ts    |  10 +-
 packages/backend/src/core/QueueService.ts     |   7 +
 packages/backend/src/core/RoleService.ts      |  75 ++++--
 .../backend/src/queue/QueueProcessorModule.ts |   3 +
 .../src/queue/QueueProcessorService.ts        |   3 +
 ...CheckModeratorsActivityProcessorService.ts | 127 ++++++++++
 .../server/api/endpoints/admin/show-users.ts  |   4 +-
 packages/backend/test/unit/RoleService.ts     | 150 +++++++++--
 ...CheckModeratorsActivityProcessorService.ts | 235 ++++++++++++++++++
 10 files changed, 575 insertions(+), 44 deletions(-)
 create mode 100644 packages/backend/src/queue/processors/CheckModeratorsActivityProcessorService.ts
 create mode 100644 packages/backend/test/unit/queue/processors/CheckModeratorsActivityProcessorService.ts

diff --git a/CHANGELOG.md b/CHANGELOG.md
index b449a1b91e..030dbfda28 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,10 +1,15 @@
 ## 2024.10.1
+### Note
+- 悪質なユーザからサーバを守る措置の一環として、モデレータ権限を持つユーザの最終アクティブ日時を確認し、 
+7日間活動していない場合は自動的に招待制へと移行(コントロールパネル -> モデレーション -> "誰でも新規登録できるようにする"をオフに変更)するようになりました。  
+詳細な経緯は https://github.com/misskey-dev/misskey/issues/13437 をご確認ください。
 
 ### Client
 - Enhance: l10nの更新
 - Fix: メールアドレス不要でCaptchaが有効な場合にアカウント登録完了後自動でのログインに失敗する問題を修正
 
 ### Server
+- Feat: モデレータ権限を持つユーザが全員7日間活動しなかった場合は自動的に招待制へと移行するように ( #13437 )
 - Fix: `admin/emoji/update`エンドポイントのidのみ指定した時不正なエラーが発生するバグを修正
 
 ## 2024.10.0
diff --git a/packages/backend/src/core/AbuseReportNotificationService.ts b/packages/backend/src/core/AbuseReportNotificationService.ts
index fb7c7bd2c3..7d030f2f16 100644
--- a/packages/backend/src/core/AbuseReportNotificationService.ts
+++ b/packages/backend/src/core/AbuseReportNotificationService.ts
@@ -61,7 +61,10 @@ export class AbuseReportNotificationService implements OnApplicationShutdown {
 			return;
 		}
 
-		const moderatorIds = await this.roleService.getModeratorIds(true, true);
+		const moderatorIds = await this.roleService.getModeratorIds({
+			includeAdmins: true,
+			excludeExpire: true,
+		});
 
 		for (const moderatorId of moderatorIds) {
 			for (const abuseReport of abuseReports) {
@@ -370,7 +373,10 @@ export class AbuseReportNotificationService implements OnApplicationShutdown {
 		}
 
 		// モデレータ権限の有無で通知先設定を振り分ける
-		const authorizedUserIds = await this.roleService.getModeratorIds(true, true);
+		const authorizedUserIds = await this.roleService.getModeratorIds({
+			includeAdmins: true,
+			excludeExpire: true,
+		});
 		const authorizedUserRecipients = Array.of<MiAbuseReportNotificationRecipient>();
 		const unauthorizedUserRecipients = Array.of<MiAbuseReportNotificationRecipient>();
 		for (const recipient of userRecipients) {
diff --git a/packages/backend/src/core/QueueService.ts b/packages/backend/src/core/QueueService.ts
index f35e456556..37028026cc 100644
--- a/packages/backend/src/core/QueueService.ts
+++ b/packages/backend/src/core/QueueService.ts
@@ -93,6 +93,13 @@ export class QueueService {
 			repeat: { pattern: '0 0 * * *' },
 			removeOnComplete: true,
 		});
+
+		this.systemQueue.add('checkModeratorsActivity', {
+		}, {
+			// 毎時30分に起動
+			repeat: { pattern: '30 * * * *' },
+			removeOnComplete: true,
+		});
 	}
 
 	@bindThis
diff --git a/packages/backend/src/core/RoleService.ts b/packages/backend/src/core/RoleService.ts
index 583eea1a34..5af6b05942 100644
--- a/packages/backend/src/core/RoleService.ts
+++ b/packages/backend/src/core/RoleService.ts
@@ -101,6 +101,7 @@ export const DEFAULT_POLICIES: RolePolicies = {
 
 @Injectable()
 export class RoleService implements OnApplicationShutdown, OnModuleInit {
+	private rootUserIdCache: MemorySingleCache<MiUser['id']>;
 	private rolesCache: MemorySingleCache<MiRole[]>;
 	private roleAssignmentByUserIdCache: MemoryKVCache<MiRoleAssignment[]>;
 	private notificationService: NotificationService;
@@ -136,6 +137,7 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
 		private moderationLogService: ModerationLogService,
 		private fanoutTimelineService: FanoutTimelineService,
 	) {
+		this.rootUserIdCache = new MemorySingleCache<MiUser['id']>(1000 * 60 * 60 * 24 * 7); // 1week. rootユーザのIDは不変なので長めに
 		this.rolesCache = new MemorySingleCache<MiRole[]>(1000 * 60 * 60); // 1h
 		this.roleAssignmentByUserIdCache = new MemoryKVCache<MiRoleAssignment[]>(1000 * 60 * 5); // 5m
 
@@ -416,49 +418,78 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
 	}
 
 	@bindThis
-	public async isExplorable(role: { id: MiRole['id']} | null): Promise<boolean> {
+	public async isExplorable(role: { id: MiRole['id'] } | null): Promise<boolean> {
 		if (role == null) return false;
 		const check = await this.rolesRepository.findOneBy({ id: role.id });
 		if (check == null) return false;
 		return check.isExplorable;
 	}
 
+	/**
+	 * モデレーター権限のロールが割り当てられているユーザID一覧を取得する.
+	 *
+	 * @param opts.includeAdmins 管理者権限も含めるか(デフォルト: true)
+	 * @param opts.includeRoot rootユーザも含めるか(デフォルト: false)
+	 * @param opts.excludeExpire 期限切れのロールを除外するか(デフォルト: false)
+	 */
 	@bindThis
-	public async getModeratorIds(includeAdmins = true, excludeExpire = false): Promise<MiUser['id'][]> {
+	public async getModeratorIds(opts?: {
+		includeAdmins?: boolean,
+		includeRoot?: boolean,
+		excludeExpire?: boolean,
+	}): Promise<MiUser['id'][]> {
+		const includeAdmins = opts?.includeAdmins ?? true;
+		const includeRoot = opts?.includeRoot ?? false;
+		const excludeExpire = opts?.excludeExpire ?? false;
+
 		const roles = await this.rolesCache.fetch(() => this.rolesRepository.findBy({}));
 		const moderatorRoles = includeAdmins
 			? roles.filter(r => r.isModerator || r.isAdministrator)
 			: roles.filter(r => r.isModerator);
 
-		// TODO: isRootなアカウントも含める
 		const assigns = moderatorRoles.length > 0
 			? await this.roleAssignmentsRepository.findBy({ roleId: In(moderatorRoles.map(r => r.id)) })
 			: [];
 
+		// Setを経由して重複を除去(ユーザIDは重複する可能性があるので)
 		const now = Date.now();
-		const result = [
-			// Setを経由して重複を除去(ユーザIDは重複する可能性があるので)
-			...new Set(
-				assigns
-					.filter(it =>
-						(excludeExpire)
-							? (it.expiresAt == null || it.expiresAt.getTime() > now)
-							: true,
-					)
-					.map(a => a.userId),
-			),
-		];
+		const resultSet = new Set(
+			assigns
+				.filter(it =>
+					(excludeExpire)
+						? (it.expiresAt == null || it.expiresAt.getTime() > now)
+						: true,
+				)
+				.map(a => a.userId),
+		);
 
-		return result.sort((x, y) => x.localeCompare(y));
+		if (includeRoot) {
+			const rootUserId = await this.rootUserIdCache.fetch(async () => {
+				const it = await this.usersRepository.createQueryBuilder('users')
+					.select('id')
+					.where({ isRoot: true })
+					.getRawOne<{ id: string }>();
+				// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+				return it!.id;
+			});
+			resultSet.add(rootUserId);
+		}
+
+		return [...resultSet].sort((x, y) => x.localeCompare(y));
 	}
 
 	@bindThis
-	public async getModerators(includeAdmins = true): Promise<MiUser[]> {
-		const ids = await this.getModeratorIds(includeAdmins);
-		const users = ids.length > 0 ? await this.usersRepository.findBy({
-			id: In(ids),
-		}) : [];
-		return users;
+	public async getModerators(opts?: {
+		includeAdmins?: boolean,
+		includeRoot?: boolean,
+		excludeExpire?: boolean,
+	}): Promise<MiUser[]> {
+		const ids = await this.getModeratorIds(opts);
+		return ids.length > 0
+			? await this.usersRepository.findBy({
+				id: In(ids),
+			})
+			: [];
 	}
 
 	@bindThis
diff --git a/packages/backend/src/queue/QueueProcessorModule.ts b/packages/backend/src/queue/QueueProcessorModule.ts
index 0027b5ef3d..9044285bf6 100644
--- a/packages/backend/src/queue/QueueProcessorModule.ts
+++ b/packages/backend/src/queue/QueueProcessorModule.ts
@@ -6,6 +6,7 @@
 import { Module } from '@nestjs/common';
 import { CoreModule } from '@/core/CoreModule.js';
 import { GlobalModule } from '@/GlobalModule.js';
+import { CheckModeratorsActivityProcessorService } from '@/queue/processors/CheckModeratorsActivityProcessorService.js';
 import { QueueLoggerService } from './QueueLoggerService.js';
 import { QueueProcessorService } from './QueueProcessorService.js';
 import { DeliverProcessorService } from './processors/DeliverProcessorService.js';
@@ -80,6 +81,8 @@ import { RelationshipProcessorService } from './processors/RelationshipProcessor
 		DeliverProcessorService,
 		InboxProcessorService,
 		AggregateRetentionProcessorService,
+		CheckExpiredMutingsProcessorService,
+		CheckModeratorsActivityProcessorService,
 		QueueProcessorService,
 	],
 	exports: [
diff --git a/packages/backend/src/queue/QueueProcessorService.ts b/packages/backend/src/queue/QueueProcessorService.ts
index e9e1c45224..85e148e900 100644
--- a/packages/backend/src/queue/QueueProcessorService.ts
+++ b/packages/backend/src/queue/QueueProcessorService.ts
@@ -10,6 +10,7 @@ import type { Config } from '@/config.js';
 import { DI } from '@/di-symbols.js';
 import type Logger from '@/logger.js';
 import { bindThis } from '@/decorators.js';
+import { CheckModeratorsActivityProcessorService } from '@/queue/processors/CheckModeratorsActivityProcessorService.js';
 import { UserWebhookDeliverProcessorService } from './processors/UserWebhookDeliverProcessorService.js';
 import { SystemWebhookDeliverProcessorService } from './processors/SystemWebhookDeliverProcessorService.js';
 import { EndedPollNotificationProcessorService } from './processors/EndedPollNotificationProcessorService.js';
@@ -120,6 +121,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
 		private aggregateRetentionProcessorService: AggregateRetentionProcessorService,
 		private checkExpiredMutingsProcessorService: CheckExpiredMutingsProcessorService,
 		private bakeBufferedReactionsProcessorService: BakeBufferedReactionsProcessorService,
+		private checkModeratorsActivityProcessorService: CheckModeratorsActivityProcessorService,
 		private cleanProcessorService: CleanProcessorService,
 	) {
 		this.logger = this.queueLoggerService.logger;
@@ -150,6 +152,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
 					case 'aggregateRetention': return this.aggregateRetentionProcessorService.process();
 					case 'checkExpiredMutings': return this.checkExpiredMutingsProcessorService.process();
 					case 'bakeBufferedReactions': return this.bakeBufferedReactionsProcessorService.process();
+					case 'checkModeratorsActivity': return this.checkModeratorsActivityProcessorService.process();
 					case 'clean': return this.cleanProcessorService.process();
 					default: throw new Error(`unrecognized job type ${job.name} for system`);
 				}
diff --git a/packages/backend/src/queue/processors/CheckModeratorsActivityProcessorService.ts b/packages/backend/src/queue/processors/CheckModeratorsActivityProcessorService.ts
new file mode 100644
index 0000000000..f2677f8e5c
--- /dev/null
+++ b/packages/backend/src/queue/processors/CheckModeratorsActivityProcessorService.ts
@@ -0,0 +1,127 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Injectable } from '@nestjs/common';
+import type Logger from '@/logger.js';
+import { bindThis } from '@/decorators.js';
+import { MetaService } from '@/core/MetaService.js';
+import { RoleService } from '@/core/RoleService.js';
+import { QueueLoggerService } from '../QueueLoggerService.js';
+
+// モデレーターが不在と判断する日付の閾値
+const MODERATOR_INACTIVITY_LIMIT_DAYS = 7;
+const ONE_DAY_MILLI_SEC = 1000 * 60 * 60 * 24;
+
+@Injectable()
+export class CheckModeratorsActivityProcessorService {
+	private logger: Logger;
+
+	constructor(
+		private metaService: MetaService,
+		private roleService: RoleService,
+		private queueLoggerService: QueueLoggerService,
+	) {
+		this.logger = this.queueLoggerService.logger.createSubLogger('check-moderators-activity');
+	}
+
+	@bindThis
+	public async process(): Promise<void> {
+		this.logger.info('start.');
+
+		const meta = await this.metaService.fetch(false);
+		if (!meta.disableRegistration) {
+			await this.processImpl();
+		} else {
+			this.logger.info('is already invitation only.');
+		}
+
+		this.logger.succ('finish.');
+	}
+
+	@bindThis
+	private async processImpl() {
+		const { isModeratorsInactive, inactivityLimitCountdown } = await this.evaluateModeratorsInactiveDays();
+		if (isModeratorsInactive) {
+			this.logger.warn(`The moderator has been inactive for ${MODERATOR_INACTIVITY_LIMIT_DAYS} days. We will move to invitation only.`);
+			await this.changeToInvitationOnly();
+
+			// TODO: モデレータに通知メール+Misskey通知
+			// TODO: SystemWebhook通知
+		} else {
+			if (inactivityLimitCountdown <= 2) {
+				this.logger.warn(`A moderator has been inactive for a period of time. If you are inactive for an additional ${inactivityLimitCountdown} days, it will switch to invitation only.`);
+
+				// TODO: 警告メール
+			}
+		}
+	}
+
+	/**
+	 * モデレーターが不在であるかどうかを確認する。trueの場合はモデレーターが不在である。
+	 * isModerator, isAdministrator, isRootのいずれかがtrueのユーザを対象に、
+	 * {@link MiUser.lastActiveDate}の値が実行日時の{@link MODERATOR_INACTIVITY_LIMIT_DAYS}日前よりも古いユーザがいるかどうかを確認する。
+	 * {@link MiUser.lastActiveDate}がnullの場合は、そのユーザは確認の対象外とする。
+	 *
+	 * -----
+	 *
+	 * ### サンプルパターン
+	 * - 実行日時: 2022-01-30 12:00:00
+	 * - 判定基準: 2022-01-23 12:00:00(実行日時の{@link MODERATOR_INACTIVITY_LIMIT_DAYS}日前)
+	 *
+	 * #### パターン①
+	 * - モデレータA: lastActiveDate = 2022-01-20 00:00:00 ※アウト
+	 * - モデレータB: lastActiveDate = 2022-01-23 12:00:00 ※セーフ(判定基準と同値なのでギリギリ残り0日)
+	 * - モデレータC: lastActiveDate = 2022-01-23 11:59:59 ※アウト(残り-1日)
+	 * - モデレータD: lastActiveDate = null
+	 *
+	 * この場合、モデレータBのアクティビティのみ判定基準日よりも古くないため、モデレーターが在席と判断される。
+	 *
+	 * #### パターン②
+	 * - モデレータA: lastActiveDate = 2022-01-20 00:00:00 ※アウト
+	 * - モデレータB: lastActiveDate = 2022-01-22 12:00:00 ※アウト(残り-1日)
+	 * - モデレータC: lastActiveDate = 2022-01-23 11:59:59 ※アウト(残り-1日)
+	 * - モデレータD: lastActiveDate = null
+	 *
+	 * この場合、モデレータA, B, Cのアクティビティは判定基準日よりも古いため、モデレーターが不在と判断される。
+	 */
+	@bindThis
+	public async evaluateModeratorsInactiveDays() {
+		const today = new Date();
+		const inactivePeriod = new Date(today);
+		inactivePeriod.setDate(today.getDate() - MODERATOR_INACTIVITY_LIMIT_DAYS);
+
+		const moderators = await this.fetchModerators()
+			.then(it => it.filter(it => it.lastActiveDate != null));
+		const inactiveModerators = moderators
+			// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+			.filter(it => it.lastActiveDate!.getTime() < inactivePeriod.getTime());
+
+		// 残りの猶予を示したいので、最終アクティブ日時が一番若いモデレータの日数を基準に猶予を計算する
+		// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+		const newestLastActiveDate = new Date(Math.max(...moderators.map(it => it.lastActiveDate!.getTime())));
+		const inactivityLimitCountdown = Math.floor((newestLastActiveDate.getTime() - inactivePeriod.getTime()) / ONE_DAY_MILLI_SEC);
+
+		return {
+			isModeratorsInactive: inactiveModerators.length === moderators.length,
+			inactiveModerators,
+			inactivityLimitCountdown,
+		};
+	}
+
+	@bindThis
+	private async changeToInvitationOnly() {
+		await this.metaService.update({ disableRegistration: true });
+	}
+
+	@bindThis
+	private async fetchModerators() {
+		// TODO: モデレーター以外にも特別な権限を持つユーザーがいる場合は考慮する
+		return this.roleService.getModerators({
+			includeAdmins: true,
+			includeRoot: true,
+			excludeExpire: true,
+		});
+	}
+}
diff --git a/packages/backend/src/server/api/endpoints/admin/show-users.ts b/packages/backend/src/server/api/endpoints/admin/show-users.ts
index 2fef9abbf9..2b2c8c60ab 100644
--- a/packages/backend/src/server/api/endpoints/admin/show-users.ts
+++ b/packages/backend/src/server/api/endpoints/admin/show-users.ts
@@ -71,13 +71,13 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 					break;
 				}
 				case 'moderator': {
-					const moderatorIds = await this.roleService.getModeratorIds(false);
+					const moderatorIds = await this.roleService.getModeratorIds({ includeAdmins: false });
 					if (moderatorIds.length === 0) return [];
 					query.where('user.id IN (:...moderatorIds)', { moderatorIds: moderatorIds });
 					break;
 				}
 				case 'adminOrModerator': {
-					const adminOrModeratorIds = await this.roleService.getModeratorIds();
+					const adminOrModeratorIds = await this.roleService.getModeratorIds({ includeAdmins: true });
 					if (adminOrModeratorIds.length === 0) return [];
 					query.where('user.id IN (:...adminOrModeratorIds)', { adminOrModeratorIds: adminOrModeratorIds });
 					break;
diff --git a/packages/backend/test/unit/RoleService.ts b/packages/backend/test/unit/RoleService.ts
index ef80d25f81..9c1b1008d6 100644
--- a/packages/backend/test/unit/RoleService.ts
+++ b/packages/backend/test/unit/RoleService.ts
@@ -10,6 +10,8 @@ import { jest } from '@jest/globals';
 import { ModuleMocker } from 'jest-mock';
 import { Test } from '@nestjs/testing';
 import * as lolex from '@sinonjs/fake-timers';
+import type { TestingModule } from '@nestjs/testing';
+import type { MockFunctionMetadata } from 'jest-mock';
 import { GlobalModule } from '@/GlobalModule.js';
 import { RoleService } from '@/core/RoleService.js';
 import {
@@ -31,8 +33,6 @@ import { secureRndstr } from '@/misc/secure-rndstr.js';
 import { NotificationService } from '@/core/NotificationService.js';
 import { RoleCondFormulaValue } from '@/models/Role.js';
 import { UserEntityService } from '@/core/entities/UserEntityService.js';
-import type { TestingModule } from '@nestjs/testing';
-import type { MockFunctionMetadata } from 'jest-mock';
 
 const moduleMocker = new ModuleMocker(global);
 
@@ -277,9 +277,9 @@ describe('RoleService', () => {
 	});
 
 	describe('getModeratorIds', () => {
-		test('includeAdmins = false, excludeExpire = false', async () => {
-			const [adminUser1, adminUser2, modeUser1, modeUser2, normalUser1, normalUser2] = await Promise.all([
-				createUser(), createUser(), createUser(), createUser(), createUser(), createUser(),
+		test('includeAdmins = false, includeRoot = false, excludeExpire = false', async () => {
+			const [adminUser1, adminUser2, modeUser1, modeUser2, normalUser1, normalUser2, rootUser] = await Promise.all([
+				createUser(), createUser(), createUser(), createUser(), createUser(), createUser(), createUser({ isRoot: true }),
 			]);
 
 			const role1 = await createRole({ name: 'admin', isAdministrator: true });
@@ -295,13 +295,17 @@ describe('RoleService', () => {
 				assignRole({ userId: normalUser2.id, roleId: role3.id, expiresAt: new Date(Date.now() - 1000) }),
 			]);
 
-			const result = await roleService.getModeratorIds(false, false);
+			const result = await roleService.getModeratorIds({
+				includeAdmins: false,
+				includeRoot: false,
+				excludeExpire: false,
+			});
 			expect(result).toEqual([modeUser1.id, modeUser2.id]);
 		});
 
-		test('includeAdmins = false, excludeExpire = true', async () => {
-			const [adminUser1, adminUser2, modeUser1, modeUser2, normalUser1, normalUser2] = await Promise.all([
-				createUser(), createUser(), createUser(), createUser(), createUser(), createUser(),
+		test('includeAdmins = false, includeRoot = false, excludeExpire = true', async () => {
+			const [adminUser1, adminUser2, modeUser1, modeUser2, normalUser1, normalUser2, rootUser] = await Promise.all([
+				createUser(), createUser(), createUser(), createUser(), createUser(), createUser(), createUser({ isRoot: true }),
 			]);
 
 			const role1 = await createRole({ name: 'admin', isAdministrator: true });
@@ -317,13 +321,17 @@ describe('RoleService', () => {
 				assignRole({ userId: normalUser2.id, roleId: role3.id, expiresAt: new Date(Date.now() - 1000) }),
 			]);
 
-			const result = await roleService.getModeratorIds(false, true);
+			const result = await roleService.getModeratorIds({
+				includeAdmins: false,
+				includeRoot: false,
+				excludeExpire: true,
+			});
 			expect(result).toEqual([modeUser1.id]);
 		});
 
-		test('includeAdmins = true, excludeExpire = false', async () => {
-			const [adminUser1, adminUser2, modeUser1, modeUser2, normalUser1, normalUser2] = await Promise.all([
-				createUser(), createUser(), createUser(), createUser(), createUser(), createUser(),
+		test('includeAdmins = true, includeRoot = false, excludeExpire = false', async () => {
+			const [adminUser1, adminUser2, modeUser1, modeUser2, normalUser1, normalUser2, rootUser] = await Promise.all([
+				createUser(), createUser(), createUser(), createUser(), createUser(), createUser(), createUser({ isRoot: true }),
 			]);
 
 			const role1 = await createRole({ name: 'admin', isAdministrator: true });
@@ -339,13 +347,17 @@ describe('RoleService', () => {
 				assignRole({ userId: normalUser2.id, roleId: role3.id, expiresAt: new Date(Date.now() - 1000) }),
 			]);
 
-			const result = await roleService.getModeratorIds(true, false);
+			const result = await roleService.getModeratorIds({
+				includeAdmins: true,
+				includeRoot: false,
+				excludeExpire: false,
+			});
 			expect(result).toEqual([adminUser1.id, adminUser2.id, modeUser1.id, modeUser2.id]);
 		});
 
-		test('includeAdmins = true, excludeExpire = true', async () => {
-			const [adminUser1, adminUser2, modeUser1, modeUser2, normalUser1, normalUser2] = await Promise.all([
-				createUser(), createUser(), createUser(), createUser(), createUser(), createUser(),
+		test('includeAdmins = true, includeRoot = false, excludeExpire = true', async () => {
+			const [adminUser1, adminUser2, modeUser1, modeUser2, normalUser1, normalUser2, rootUser] = await Promise.all([
+				createUser(), createUser(), createUser(), createUser(), createUser(), createUser(), createUser({ isRoot: true }),
 			]);
 
 			const role1 = await createRole({ name: 'admin', isAdministrator: true });
@@ -361,9 +373,111 @@ describe('RoleService', () => {
 				assignRole({ userId: normalUser2.id, roleId: role3.id, expiresAt: new Date(Date.now() - 1000) }),
 			]);
 
-			const result = await roleService.getModeratorIds(true, true);
+			const result = await roleService.getModeratorIds({
+				includeAdmins: true,
+				includeRoot: false,
+				excludeExpire: true,
+			});
 			expect(result).toEqual([adminUser1.id, modeUser1.id]);
 		});
+
+		test('includeAdmins = false, includeRoot = true, excludeExpire = false', async () => {
+			const [adminUser1, adminUser2, modeUser1, modeUser2, normalUser1, normalUser2, rootUser] = await Promise.all([
+				createUser(), createUser(), createUser(), createUser(), createUser(), createUser(), createUser({ isRoot: true }),
+			]);
+
+			const role1 = await createRole({ name: 'admin', isAdministrator: true });
+			const role2 = await createRole({ name: 'moderator', isModerator: true });
+			const role3 = await createRole({ name: 'normal' });
+
+			await Promise.all([
+				assignRole({ userId: adminUser1.id, roleId: role1.id }),
+				assignRole({ userId: adminUser2.id, roleId: role1.id, expiresAt: new Date(Date.now() - 1000) }),
+				assignRole({ userId: modeUser1.id, roleId: role2.id }),
+				assignRole({ userId: modeUser2.id, roleId: role2.id, expiresAt: new Date(Date.now() - 1000) }),
+				assignRole({ userId: normalUser1.id, roleId: role3.id }),
+				assignRole({ userId: normalUser2.id, roleId: role3.id, expiresAt: new Date(Date.now() - 1000) }),
+			]);
+
+			const result = await roleService.getModeratorIds({
+				includeAdmins: false,
+				includeRoot: true,
+				excludeExpire: false,
+			});
+			expect(result).toEqual([modeUser1.id, modeUser2.id, rootUser.id]);
+		});
+
+		test('root has moderator role', async () => {
+			const [adminUser1, modeUser1, normalUser1, rootUser] = await Promise.all([
+				createUser(), createUser(), createUser(), createUser({ isRoot: true }),
+			]);
+
+			const role1 = await createRole({ name: 'admin', isAdministrator: true });
+			const role2 = await createRole({ name: 'moderator', isModerator: true });
+			const role3 = await createRole({ name: 'normal' });
+
+			await Promise.all([
+				assignRole({ userId: adminUser1.id, roleId: role1.id }),
+				assignRole({ userId: modeUser1.id, roleId: role2.id }),
+				assignRole({ userId: rootUser.id, roleId: role2.id }),
+				assignRole({ userId: normalUser1.id, roleId: role3.id }),
+			]);
+
+			const result = await roleService.getModeratorIds({
+				includeAdmins: false,
+				includeRoot: true,
+				excludeExpire: false,
+			});
+			expect(result).toEqual([modeUser1.id, rootUser.id]);
+		});
+
+		test('root has administrator role', async () => {
+			const [adminUser1, modeUser1, normalUser1, rootUser] = await Promise.all([
+				createUser(), createUser(), createUser(), createUser({ isRoot: true }),
+			]);
+
+			const role1 = await createRole({ name: 'admin', isAdministrator: true });
+			const role2 = await createRole({ name: 'moderator', isModerator: true });
+			const role3 = await createRole({ name: 'normal' });
+
+			await Promise.all([
+				assignRole({ userId: adminUser1.id, roleId: role1.id }),
+				assignRole({ userId: rootUser.id, roleId: role1.id }),
+				assignRole({ userId: modeUser1.id, roleId: role2.id }),
+				assignRole({ userId: normalUser1.id, roleId: role3.id }),
+			]);
+
+			const result = await roleService.getModeratorIds({
+				includeAdmins: true,
+				includeRoot: true,
+				excludeExpire: false,
+			});
+			expect(result).toEqual([adminUser1.id, modeUser1.id, rootUser.id]);
+		});
+
+		test('root has moderator role(expire)', async () => {
+			const [adminUser1, modeUser1, normalUser1, rootUser] = await Promise.all([
+				createUser(), createUser(), createUser(), createUser({ isRoot: true }),
+			]);
+
+			const role1 = await createRole({ name: 'admin', isAdministrator: true });
+			const role2 = await createRole({ name: 'moderator', isModerator: true });
+			const role3 = await createRole({ name: 'normal' });
+
+			await Promise.all([
+				assignRole({ userId: adminUser1.id, roleId: role1.id }),
+				assignRole({ userId: modeUser1.id, roleId: role2.id, expiresAt: new Date(Date.now() - 1000) }),
+				assignRole({ userId: rootUser.id, roleId: role2.id, expiresAt: new Date(Date.now() - 1000) }),
+				assignRole({ userId: normalUser1.id, roleId: role3.id }),
+			]);
+
+			const result = await roleService.getModeratorIds({
+				includeAdmins: false,
+				includeRoot: true,
+				excludeExpire: true,
+			});
+			expect(result).toEqual([rootUser.id]);
+		});
 	});
 
 	describe('conditional role', () => {
diff --git a/packages/backend/test/unit/queue/processors/CheckModeratorsActivityProcessorService.ts b/packages/backend/test/unit/queue/processors/CheckModeratorsActivityProcessorService.ts
new file mode 100644
index 0000000000..b783320aa0
--- /dev/null
+++ b/packages/backend/test/unit/queue/processors/CheckModeratorsActivityProcessorService.ts
@@ -0,0 +1,235 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { jest } from '@jest/globals';
+import { Test, TestingModule } from '@nestjs/testing';
+import * as lolex from '@sinonjs/fake-timers';
+import { addHours, addSeconds, subDays, subHours, subSeconds } from 'date-fns';
+import { CheckModeratorsActivityProcessorService } from '@/queue/processors/CheckModeratorsActivityProcessorService.js';
+import { MiUser, UserProfilesRepository, UsersRepository } from '@/models/_.js';
+import { IdService } from '@/core/IdService.js';
+import { RoleService } from '@/core/RoleService.js';
+import { GlobalModule } from '@/GlobalModule.js';
+import { MetaService } from '@/core/MetaService.js';
+import { DI } from '@/di-symbols.js';
+import { QueueLoggerService } from '@/queue/QueueLoggerService.js';
+
+const baseDate = new Date(Date.UTC(2000, 11, 15, 12, 0, 0));
+
+describe('CheckModeratorsActivityProcessorService', () => {
+	let app: TestingModule;
+	let clock: lolex.InstalledClock;
+	let service: CheckModeratorsActivityProcessorService;
+
+	// --------------------------------------------------------------------------------------
+
+	let usersRepository: UsersRepository;
+	let userProfilesRepository: UserProfilesRepository;
+	let idService: IdService;
+	let roleService: jest.Mocked<RoleService>;
+
+	// --------------------------------------------------------------------------------------
+
+	async function createUser(data: Partial<MiUser> = {}) {
+		const id = idService.gen();
+		const user = await usersRepository
+			.insert({
+				id: id,
+				username: `user_${id}`,
+				usernameLower: `user_${id}`.toLowerCase(),
+				...data,
+			})
+			.then(x => usersRepository.findOneByOrFail(x.identifiers[0]));
+
+		await userProfilesRepository.insert({
+			userId: user.id,
+		});
+
+		return user;
+	}
+
+	function mockModeratorRole(users: MiUser[]) {
+		roleService.getModerators.mockReset();
+		roleService.getModerators.mockResolvedValue(users);
+	}
+
+	// --------------------------------------------------------------------------------------
+
+	beforeAll(async () => {
+		app = await Test
+			.createTestingModule({
+				imports: [
+					GlobalModule,
+				],
+				providers: [
+					CheckModeratorsActivityProcessorService,
+					IdService,
+					{
+						provide: RoleService, useFactory: () => ({ getModerators: jest.fn() }),
+					},
+					{
+						provide: MetaService, useFactory: () => ({ fetch: jest.fn() }),
+					},
+					{
+						provide: QueueLoggerService, useFactory: () => ({
+							logger: ({
+								createSubLogger: () => ({
+									info: jest.fn(),
+									warn: jest.fn(),
+									succ: jest.fn(),
+								}),
+							}),
+						}),
+					},
+				],
+			})
+			.compile();
+
+		usersRepository = app.get(DI.usersRepository);
+		userProfilesRepository = app.get(DI.userProfilesRepository);
+
+		service = app.get(CheckModeratorsActivityProcessorService);
+		idService = app.get(IdService);
+		roleService = app.get(RoleService) as jest.Mocked<RoleService>;
+
+		app.enableShutdownHooks();
+	});
+
+	beforeEach(async () => {
+		clock = lolex.install({
+			now: new Date(baseDate),
+			shouldClearNativeTimers: true,
+		});
+	});
+
+	afterEach(async () => {
+		clock.uninstall();
+		await usersRepository.delete({});
+		await userProfilesRepository.delete({});
+		roleService.getModerators.mockReset();
+	});
+
+	afterAll(async () => {
+		await app.close();
+	});
+
+	// --------------------------------------------------------------------------------------
+
+	describe('evaluateModeratorsInactiveDays', () => {
+		test('[isModeratorsInactive] inactiveなモデレーターがいても他のモデレーターがアクティブなら"運営が非アクティブ"としてみなされない', async () => {
+			const [user1, user2, user3, user4] = await Promise.all([
+				// 期限よりも1秒新しいタイミングでアクティブ化(セーフ)
+				createUser({ lastActiveDate: subDays(addSeconds(baseDate, 1), 7) }),
+				// 期限ちょうどにアクティブ化(セーフ)
+				createUser({ lastActiveDate: subDays(baseDate, 7) }),
+				// 期限よりも1秒古いタイミングでアクティブ化(アウト)
+				createUser({ lastActiveDate: subDays(subSeconds(baseDate, 1), 7) }),
+				// 対象外
+				createUser({ lastActiveDate: null }),
+			]);
+
+			mockModeratorRole([user1, user2, user3, user4]);
+
+			const result = await service.evaluateModeratorsInactiveDays();
+			expect(result.isModeratorsInactive).toBe(false);
+			expect(result.inactiveModerators).toEqual([user3]);
+		});
+
+		test('[isModeratorsInactive] 全員非アクティブなら"運営が非アクティブ"としてみなされる', async () => {
+			const [user1, user2] = await Promise.all([
+				// 期限よりも1秒古いタイミングでアクティブ化(アウト)
+				createUser({ lastActiveDate: subDays(subSeconds(baseDate, 1), 7) }),
+				// 対象外
+				createUser({ lastActiveDate: null }),
+			]);
+
+			mockModeratorRole([user1, user2]);
+
+			const result = await service.evaluateModeratorsInactiveDays();
+			expect(result.isModeratorsInactive).toBe(true);
+			expect(result.inactiveModerators).toEqual([user1]);
+		});
+
+		test('[countdown] 猶予まで24時間ある場合、猶予1日として計算される', async () => {
+			const [user1, user2] = await Promise.all([
+				createUser({ lastActiveDate: subDays(baseDate, 8) }),
+				// 猶予はこのユーザ基準で計算される想定。
+				// 期限まで残り24時間->猶予1日として計算されるはずである
+				createUser({ lastActiveDate: subDays(baseDate, 6) }),
+			]);
+
+			mockModeratorRole([user1, user2]);
+
+			const result = await service.evaluateModeratorsInactiveDays();
+			expect(result.isModeratorsInactive).toBe(false);
+			expect(result.inactiveModerators).toEqual([user1]);
+			expect(result.inactivityLimitCountdown).toBe(1);
+		});
+
+		test('[countdown] 猶予まで25時間ある場合、猶予1日として計算される', async () => {
+			const [user1, user2] = await Promise.all([
+				createUser({ lastActiveDate: subDays(baseDate, 8) }),
+				// 猶予はこのユーザ基準で計算される想定。
+				// 期限まで残り25時間->猶予1日として計算されるはずである
+				createUser({ lastActiveDate: subDays(addHours(baseDate, 1), 6) }),
+			]);
+
+			mockModeratorRole([user1, user2]);
+
+			const result = await service.evaluateModeratorsInactiveDays();
+			expect(result.isModeratorsInactive).toBe(false);
+			expect(result.inactiveModerators).toEqual([user1]);
+			expect(result.inactivityLimitCountdown).toBe(1);
+		});
+
+		test('[countdown] 猶予まで23時間ある場合、猶予0日として計算される', async () => {
+			const [user1, user2] = await Promise.all([
+				createUser({ lastActiveDate: subDays(baseDate, 8) }),
+				// 猶予はこのユーザ基準で計算される想定。
+				// 期限まで残り23時間->猶予0日として計算されるはずである
+				createUser({ lastActiveDate: subDays(subHours(baseDate, 1), 6) }),
+			]);
+
+			mockModeratorRole([user1, user2]);
+
+			const result = await service.evaluateModeratorsInactiveDays();
+			expect(result.isModeratorsInactive).toBe(false);
+			expect(result.inactiveModerators).toEqual([user1]);
+			expect(result.inactivityLimitCountdown).toBe(0);
+		});
+
+		test('[countdown] 期限ちょうどの場合、猶予0日として計算される', async () => {
+			const [user1, user2] = await Promise.all([
+				createUser({ lastActiveDate: subDays(baseDate, 8) }),
+				// 猶予はこのユーザ基準で計算される想定。
+				// 期限ちょうど->猶予0日として計算されるはずである
+				createUser({ lastActiveDate: subDays(baseDate, 7) }),
+			]);
+
+			mockModeratorRole([user1, user2]);
+
+			const result = await service.evaluateModeratorsInactiveDays();
+			expect(result.isModeratorsInactive).toBe(false);
+			expect(result.inactiveModerators).toEqual([user1]);
+			expect(result.inactivityLimitCountdown).toBe(0);
+		});
+
+		test('[countdown] 期限より1時間超過している場合、猶予-1日として計算される', async () => {
+			const [user1, user2] = await Promise.all([
+				createUser({ lastActiveDate: subDays(baseDate, 8) }),
+				// 猶予はこのユーザ基準で計算される想定。
+				// 期限より1時間超過->猶予-1日として計算されるはずである
+				createUser({ lastActiveDate: subDays(subHours(baseDate, 1), 7) }),
+			]);
+
+			mockModeratorRole([user1, user2]);
+
+			const result = await service.evaluateModeratorsInactiveDays();
+			expect(result.isModeratorsInactive).toBe(true);
+			expect(result.inactiveModerators).toEqual([user1, user2]);
+			expect(result.inactivityLimitCountdown).toBe(-1);
+		});
+	});
+});

From c397b42242a34b85de1c183d86ee78c5cd50e161 Mon Sep 17 00:00:00 2001
From: syuilo <4439005+syuilo@users.noreply.github.com>
Date: Fri, 11 Oct 2024 21:01:50 +0900
Subject: [PATCH 084/121] chore: add description

---
 locales/ja-JP.yml                                | 1 +
 packages/frontend/src/pages/admin/moderation.vue | 1 +
 2 files changed, 2 insertions(+)

diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml
index 0076c467ec..48a670ce50 100644
--- a/locales/ja-JP.yml
+++ b/locales/ja-JP.yml
@@ -1440,6 +1440,7 @@ _serverSettings:
   reactionsBufferingDescription: "有効にすると、リアクション作成時のパフォーマンスが大幅に向上し、データベースへの負荷を軽減することが可能です。ただし、Redisのメモリ使用量は増加します。"
   inquiryUrl: "問い合わせ先URL"
   inquiryUrlDescription: "サーバー運営者へのお問い合わせフォームのURLや、運営者の連絡先等が記載されたWebページのURLを指定します。"
+  thisSettingWillAutomaticallyOffWhenModeratorsInactive: "一定期間モデレーターのアクティビティが検出されなかった場合、スパム防止のためこの設定は自動でオフになります。"
 
 _accountMigration:
   moveFrom: "別のアカウントからこのアカウントに移行"
diff --git a/packages/frontend/src/pages/admin/moderation.vue b/packages/frontend/src/pages/admin/moderation.vue
index 54eb95cd51..04d23b1358 100644
--- a/packages/frontend/src/pages/admin/moderation.vue
+++ b/packages/frontend/src/pages/admin/moderation.vue
@@ -12,6 +12,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 				<div class="_gaps_m">
 					<MkSwitch v-model="enableRegistration" @change="onChange_enableRegistration">
 						<template #label>{{ i18n.ts.enableRegistration }}</template>
+						<template #caption>{{ i18n.ts._serverSettings.thisSettingWillAutomaticallyOffWhenModeratorsInactive }}</template>
 					</MkSwitch>
 
 					<MkSwitch v-model="emailRequiredForSignup" @change="onChange_emailRequiredForSignup">

From af1cbc131fc9e045692f9f9def708c0978817fff Mon Sep 17 00:00:00 2001
From: syuilo <4439005+syuilo@users.noreply.github.com>
Date: Fri, 11 Oct 2024 21:05:53 +0900
Subject: [PATCH 085/121] wip (#14745)

---
 locales/index.d.ts                            |   4 +++
 locales/ja-JP.yml                             |   1 +
 .../migration/1728550878802-testcaptcha.js    |  16 ++++++++++
 packages/backend/src/core/CaptchaService.ts   |  13 ++++++++
 .../src/core/entities/MetaEntityService.ts    |   1 +
 packages/backend/src/models/Meta.ts           |   5 ++++
 .../backend/src/models/json-schema/meta.ts    |   4 +++
 .../src/server/api/ApiServerService.ts        |   2 ++
 .../src/server/api/SigninApiService.ts        |   7 +++++
 .../src/server/api/SignupApiService.ts        |   7 +++++
 .../src/server/api/endpoints/admin/meta.ts    |   5 ++++
 .../server/api/endpoints/admin/update-meta.ts |   5 ++++
 packages/frontend/assets/testcaptcha.png      | Bin 0 -> 2634 bytes
 .../frontend/src/components/MkCaptcha.vue     |  28 ++++++++++++++++--
 .../src/components/MkSignin.password.vue      |   9 +++++-
 packages/frontend/src/components/MkSignin.vue |   6 ++--
 .../src/components/MkSignupDialog.form.vue    |   6 ++++
 .../src/pages/admin/bot-protection.vue        |  15 +++++++++-
 packages/misskey-js/src/autogen/types.ts      |   3 ++
 19 files changed, 130 insertions(+), 7 deletions(-)
 create mode 100644 packages/backend/migration/1728550878802-testcaptcha.js
 create mode 100644 packages/frontend/assets/testcaptcha.png

diff --git a/locales/index.d.ts b/locales/index.d.ts
index f0dead1245..dab8eb0361 100644
--- a/locales/index.d.ts
+++ b/locales/index.d.ts
@@ -5166,6 +5166,10 @@ export interface Locale extends ILocale {
      * 対象
      */
     "target": string;
+    /**
+     * CAPTCHAのテストを目的とした機能です。<strong>本番環境で使用しないでください。</strong>
+     */
+    "testCaptchaWarning": string;
     "_abuseUserReport": {
         /**
          * 転送
diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml
index 48a670ce50..440ffa9306 100644
--- a/locales/ja-JP.yml
+++ b/locales/ja-JP.yml
@@ -1287,6 +1287,7 @@ passkeyVerificationFailed: "パスキーの検証に失敗しました。"
 passkeyVerificationSucceededButPasswordlessLoginDisabled: "パスキーの検証に成功しましたが、パスワードレスログインが無効になっています。"
 messageToFollower: "フォロワーへのメッセージ"
 target: "対象"
+testCaptchaWarning: "CAPTCHAのテストを目的とした機能です。<strong>本番環境で使用しないでください。</strong>"
 
 _abuseUserReport:
   forward: "転送"
diff --git a/packages/backend/migration/1728550878802-testcaptcha.js b/packages/backend/migration/1728550878802-testcaptcha.js
new file mode 100644
index 0000000000..d8d987c0c1
--- /dev/null
+++ b/packages/backend/migration/1728550878802-testcaptcha.js
@@ -0,0 +1,16 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+export class Testcaptcha1728550878802 {
+    name = 'Testcaptcha1728550878802'
+
+    async up(queryRunner) {
+			await queryRunner.query(`ALTER TABLE "meta" ADD "enableTestcaptcha" boolean NOT NULL DEFAULT false`);
+    }
+
+    async down(queryRunner) {
+			await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "enableTestcaptcha"`);
+    }
+}
diff --git a/packages/backend/src/core/CaptchaService.ts b/packages/backend/src/core/CaptchaService.ts
index f6b7955cd2..206d0dbe0a 100644
--- a/packages/backend/src/core/CaptchaService.ts
+++ b/packages/backend/src/core/CaptchaService.ts
@@ -119,5 +119,18 @@ export class CaptchaService {
 			throw new Error(`turnstile-failed: ${errorCodes}`);
 		}
 	}
+
+	@bindThis
+	public async verifyTestcaptcha(response: string | null | undefined): Promise<void> {
+		if (response == null) {
+			throw new Error('testcaptcha-failed: no response provided');
+		}
+
+		const success = response === 'testcaptcha-passed';
+
+		if (!success) {
+			throw new Error('testcaptcha-failed');
+		}
+	}
 }
 
diff --git a/packages/backend/src/core/entities/MetaEntityService.ts b/packages/backend/src/core/entities/MetaEntityService.ts
index fbd982eb34..409dca3426 100644
--- a/packages/backend/src/core/entities/MetaEntityService.ts
+++ b/packages/backend/src/core/entities/MetaEntityService.ts
@@ -96,6 +96,7 @@ export class MetaEntityService {
 			recaptchaSiteKey: instance.recaptchaSiteKey,
 			enableTurnstile: instance.enableTurnstile,
 			turnstileSiteKey: instance.turnstileSiteKey,
+			enableTestcaptcha: instance.enableTestcaptcha,
 			swPublickey: instance.swPublicKey,
 			themeColor: instance.themeColor,
 			mascotImageUrl: instance.mascotImageUrl ?? '/assets/ai.png',
diff --git a/packages/backend/src/models/Meta.ts b/packages/backend/src/models/Meta.ts
index d29689f907..fd007de6c6 100644
--- a/packages/backend/src/models/Meta.ts
+++ b/packages/backend/src/models/Meta.ts
@@ -258,6 +258,11 @@ export class MiMeta {
 	})
 	public turnstileSecretKey: string | null;
 
+	@Column('boolean', {
+		default: false,
+	})
+	public enableTestcaptcha: boolean;
+
 	// chaptcha系を追加した際にはnodeinfoのレスポンスに追加するのを忘れないようにすること
 
 	@Column('enum', {
diff --git a/packages/backend/src/models/json-schema/meta.ts b/packages/backend/src/models/json-schema/meta.ts
index 99feeaa7d7..e3fd63464a 100644
--- a/packages/backend/src/models/json-schema/meta.ts
+++ b/packages/backend/src/models/json-schema/meta.ts
@@ -115,6 +115,10 @@ export const packedMetaLiteSchema = {
 			type: 'string',
 			optional: false, nullable: true,
 		},
+		enableTestcaptcha: {
+			type: 'boolean',
+			optional: false, nullable: false,
+		},
 		swPublickey: {
 			type: 'string',
 			optional: false, nullable: true,
diff --git a/packages/backend/src/server/api/ApiServerService.ts b/packages/backend/src/server/api/ApiServerService.ts
index be63635efe..3a8cb19f01 100644
--- a/packages/backend/src/server/api/ApiServerService.ts
+++ b/packages/backend/src/server/api/ApiServerService.ts
@@ -119,6 +119,7 @@ export class ApiServerService {
 				'g-recaptcha-response'?: string;
 				'turnstile-response'?: string;
 				'm-captcha-response'?: string;
+				'testcaptcha-response'?: string;
 			}
 		}>('/signup', (request, reply) => this.signupApiService.signup(request, reply));
 
@@ -132,6 +133,7 @@ export class ApiServerService {
 				'g-recaptcha-response'?: string;
 				'turnstile-response'?: string;
 				'm-captcha-response'?: string;
+				'testcaptcha-response'?: string;
 			};
 		}>('/signin-flow', (request, reply) => this.signinApiService.signin(request, reply));
 
diff --git a/packages/backend/src/server/api/SigninApiService.ts b/packages/backend/src/server/api/SigninApiService.ts
index 0d24ffa56a..1d983ca4bc 100644
--- a/packages/backend/src/server/api/SigninApiService.ts
+++ b/packages/backend/src/server/api/SigninApiService.ts
@@ -71,6 +71,7 @@ export class SigninApiService {
 				'g-recaptcha-response'?: string;
 				'turnstile-response'?: string;
 				'm-captcha-response'?: string;
+				'testcaptcha-response'?: string;
 			};
 		}>,
 		reply: FastifyReply,
@@ -194,6 +195,12 @@ export class SigninApiService {
 						throw new FastifyReplyError(400, err);
 					});
 				}
+
+				if (this.meta.enableTestcaptcha) {
+					await this.captchaService.verifyTestcaptcha(body['testcaptcha-response']).catch(err => {
+						throw new FastifyReplyError(400, err);
+					});
+				}
 			}
 
 			if (same) {
diff --git a/packages/backend/src/server/api/SignupApiService.ts b/packages/backend/src/server/api/SignupApiService.ts
index c499638018..3ec5e5d3e6 100644
--- a/packages/backend/src/server/api/SignupApiService.ts
+++ b/packages/backend/src/server/api/SignupApiService.ts
@@ -67,6 +67,7 @@ export class SignupApiService {
 				'g-recaptcha-response'?: string;
 				'turnstile-response'?: string;
 				'm-captcha-response'?: string;
+				'testcaptcha-response'?: string;
 			}
 		}>,
 		reply: FastifyReply,
@@ -99,6 +100,12 @@ export class SignupApiService {
 					throw new FastifyReplyError(400, err);
 				});
 			}
+
+			if (this.meta.enableTestcaptcha) {
+				await this.captchaService.verifyTestcaptcha(body['testcaptcha-response']).catch(err => {
+					throw new FastifyReplyError(400, err);
+				});
+			}
 		}
 
 		const username = body['username'];
diff --git a/packages/backend/src/server/api/endpoints/admin/meta.ts b/packages/backend/src/server/api/endpoints/admin/meta.ts
index b76ed5c524..abb3c17be3 100644
--- a/packages/backend/src/server/api/endpoints/admin/meta.ts
+++ b/packages/backend/src/server/api/endpoints/admin/meta.ts
@@ -69,6 +69,10 @@ export const meta = {
 				type: 'string',
 				optional: false, nullable: true,
 			},
+			enableTestcaptcha: {
+				type: 'boolean',
+				optional: false, nullable: false,
+			},
 			swPublickey: {
 				type: 'string',
 				optional: false, nullable: true,
@@ -555,6 +559,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 				recaptchaSiteKey: instance.recaptchaSiteKey,
 				enableTurnstile: instance.enableTurnstile,
 				turnstileSiteKey: instance.turnstileSiteKey,
+				enableTestcaptcha: instance.enableTestcaptcha,
 				swPublickey: instance.swPublicKey,
 				themeColor: instance.themeColor,
 				mascotImageUrl: instance.mascotImageUrl,
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 9ffae840b6..e97ac4e2b9 100644
--- a/packages/backend/src/server/api/endpoints/admin/update-meta.ts
+++ b/packages/backend/src/server/api/endpoints/admin/update-meta.ts
@@ -78,6 +78,7 @@ export const paramDef = {
 		enableTurnstile: { type: 'boolean' },
 		turnstileSiteKey: { type: 'string', nullable: true },
 		turnstileSecretKey: { type: 'string', nullable: true },
+		enableTestcaptcha: { type: 'boolean' },
 		sensitiveMediaDetection: { type: 'string', enum: ['none', 'all', 'local', 'remote'] },
 		sensitiveMediaDetectionSensitivity: { type: 'string', enum: ['medium', 'low', 'high', 'veryLow', 'veryHigh'] },
 		setSensitiveFlagAutomatically: { type: 'boolean' },
@@ -357,6 +358,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 				set.turnstileSecretKey = ps.turnstileSecretKey;
 			}
 
+			if (ps.enableTestcaptcha !== undefined) {
+				set.enableTestcaptcha = ps.enableTestcaptcha;
+			}
+
 			if (ps.sensitiveMediaDetection !== undefined) {
 				set.sensitiveMediaDetection = ps.sensitiveMediaDetection;
 			}
diff --git a/packages/frontend/assets/testcaptcha.png b/packages/frontend/assets/testcaptcha.png
new file mode 100644
index 0000000000000000000000000000000000000000..9bfd252b51c6057f99ff1012897c4d9e6971f897
GIT binary patch
literal 2634
zcmV-Q3bpl#P)<h;3K|Lk000e1NJLTq003kF003kN1^@s6aN?Cz00001b5ch_0Itp)
z=>Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D3ExRXK~#8N?VR6>
zRaF$nd+f2^dhAc=u{V3DPy#E6ems;9Q4|IF6hRL%qcn+?4+DxQiG|GMKr$!{X2V22
zm`o}$rHJ7W7GsJzIL0JeC%>1wY|V1*z1Kc_pL_SY%@@8n_x#vrpY_>$?Y+;r*ZZnf
z6{S@mg=rN?VQLkSso(ypUi@~kdicvL9X~U(SdGmuclQp4S_5R`>{4~#XO~n1%%G?h
zK+p>`5Zr?Tr4>LBYz=}mj$6L{Pxq{Lsue&Unz*b2(7g8RYu&Tjsa61a?5jW2;Je)B
z^wk$2e72+oRQkd3-_`9tw-rjyfW$($$Db?P0&e4&(i0%QDQlE#Kxx~U(m0T8FvvA~
zN?X(@knAw-I(|&qe&)|^t;$woK$?Tmb!1O@<nYyQ&B|6IKrAckez*;41PBIFEg<>4
zaL4g!i|fSfVl^~!p?c@tbJhF9KUdS=l+lB-18F=})c^rwx=kA0b+KBr_WOGbIQ<5b
z6-b>_^}zV$>W%NNSJ!T?TrXh#@ZPB<_SmFeuOOqLKnS=7gZqUIbA9VIS%Ji)DzsP$
z!KJrPTvyAmnqLcn)*!fy<9n%WKw?r=42+Zsg4X+<dhWCuc%OR-B@2+4pdkfWVL({a
zY2}3Mg8sz%Q)<vd?iG|QKw^SMx!fwWa;+SneLWBZw-#`VdjTa25Npt4dk1v?{<wiW
zln(cQv7iRq>ZYM21BppjvAqMbz6){9)}(IU{JVN{<5@M>RyPSH8HhDq#SG+JBXc*@
z^0;Hm29#Z&{#rfz(hq9DEp7@*G7xLJssjVUdgRfmt7@6nUg1(In2Ce=AIBsE(E=rn
z7MvovNK6HxsZJ_;`L!RrXXjH-fYcc~`k^{da;L7I0Lj^sn^qktIa60M5X-c*ZHk5R
z>RV^JXQnF|NN8G`I)3y^u~0~<kM)y*gr=o!89y`u3R=GgKpJvA){hxyl7aZ9rFGXH
zCCR$7|KOtw>UOeqoJj@(^<rAuFQJ!c0hWI9Y5zk@>o}7I$RLPvKVDAB7gTkh5KCK>
z4aoWP=c|c{iE8`y?H!Meja74VbKSjV%a!|K49*)~|4H2!Ym2e~nVOoaHf`FZ8emG6
z<&|zOTa*mM2ZLNP6r?6Cc)5)<Xi*Xn3k-6pP*R@w#u#bQqS`<X9z3W9-sj`Tk9W^5
zM=5U*tw!%yx^cUEMZanRsRIKZ9UbkKP>xLNjCIO3qhnw|(LiF-l;+ZUNu7s@7USD3
zACLYL1p|r#a`x<5HOMh8v6lc!>Knv0YfpCmtqX!=29nw{xJw*Mprqci?qqa&(qsaH
z1)dhzy56~SXSd|?R1eC@>iJjFVL&Dzn6hetO>xOGr?5MN{p0ITRUdvbp9KbF0x~i(
zq6Rg^ZBPgd9vCiMy4gKCk4!)|Z{Dm1wXX53t8JP*e0c>J4BP;gTP7fT_wH2#Ti2K%
ztV)*&$gyL`)WFs;ddKo|{r^&lo+_h}wCRbmRVE;}Zr!SO@80bU@Qxij)P2%>_UuW5
z;hK7v{zN%OCLj=?96frp+O}<5CkPnm4;?zx39z*XrHmb+bx^5mn^>8F_yky3TWEoj
zMr+3LF|sU5OJ3W=C<_oMW}HF#v*!Mo2bqJQY(OAuFCm9|OYBqSK~Pp8DYG&89zGUE
z8`n0PvI2o(dr2-2%GxG7keq84t5erDB`G@)-<qWf4-yP0I}k{&S(-3k2Mv-M0YXBs
znix~iE3RE@_y0cXQlmhi+$n63VL*)ofpVp=L5BhL1PIr{!b0b1Z7iJs0}QC=K%}Vm
z_})qa;GhlOE;RyVety0aXsb@2KHYK5IkAeZQ@@lI2yPhPyF#l*i(gvu|C|jFkcy}$
z;GEgnS#RKQ&dkirU57x%)~Vmh3IxHV;KEq7YvMuDtOd?@jxW%zI+faEvI0Si<u^|)
zn785(3^IplImZP>)(NkEOYSj^0@=EC>)pR`^NWj%CKxcvf~){xtw+i_NxOP+ztkuY
z>GAP#bvqur%f%xBLGR+*$#`rczn*mQ;=RAY2-S(MQ;K>DWO8y+-L9Deg_NRwFAO~n
z(_#FLi2=R{t|?R}4PrmlGaz{JB=;kz{4+NJfv}hieOV7>X)`W)O^hMf2IDJ5rKwRM
zbn|rMa=(^#$g<^HpulpVme<7RlGP{>1P3sv)oQ`Ha^*_rUv5{+1r&b1%Y2|ld3+x}
zm#nNn5D0$rWNNiCIJ`9@-4GBh^~B7!c+llF`MwgB6^Ls9h5<_h1}IvfW$JOwwlwo}
zCxOUvOH)=LkPHAB+kL<^^VDNG7hiOc=}v0|yxh{19f&J%)M~ARGb{&oZM-47#vQFb
z=p}TXmLB`5>_8wHI2kM-6tr^foD`uv?ONoL(pV!vEQ*<3rOJ}-N=ak2fbbcSbZIGo
z6iSt)tk|P~Sf`$$6p;Gpo6eUFBh(mNV^C8)vyRL_tT4zL6kZ1Q<2M~KJ&K<{G&Hp0
zdSsN_R4?TXz;X>&!}B#r6A&JV7|XmYUlx(AgR1L%&DJtW2(T;2uKRaggCEb2ac$=^
z0dWO_W<+F}qh(&kF?>I_C4j#3HEU(^R!-)@mgj^Tqjn9~TT0h<t{@Osg|r{s@Eon>
zWo(z_aEE3(@_aOaBS(&Owz~#wDl-&VZe9;duaxzc^~7i2cAmn5K(q?sDQz9e3dwOG
z)Jt&V{CJpxL5)Fzp_@j};M;xnJ$lg`$^=vgxgB4Bk|`_*M5|EUDWWV(`~ACYZCt3Z
zE8q}X8UGdw->;pk84Zx6^(r*R*i1lmz?)INeRz0y#b?}mG?2QzL%TVo8yTcfFF~u~
z$Kxg`I9f`gU_f%(D+2`c2BBpKUY<)hhMzM%J#9*tI4&B9r9zrqHz&)d?JjA@`|Rt@
zp&qAQ%aR@Bd0Vz@S@GFr)TL)Yw41{_cJvZCmY`y;UZR!bvgMb)vd8iJlh?9l2W_l#
zt(&C#3dCnu>avz&{n@qe{(Sp<(t6&$efw5?b~ze|7ATtAyB1cEEXQJPfOnO{*F-Uo
zLS%XPO!Dp1#HX+F+1a^s=k9@|IS54Set?3?!E&%lZQ0yf0Ax9ssii!NlI8Jh%6+bT
z;}aHs3{4`ae)L(JU6O-9wC)$OY}wiji+>%5eBi)=6~|}+z;XN-d`^+CJXd3+I#Fhj
z_onr1k`@c@AP^|#EgvjrBHE%%3#1kRd7LkRb>u70)ffTA7gS%JLMwk05Xb^Wd#4R)
zH>OP=wd3%a_YxUER~oU(2Ly_3jIeKNtTj4512Y4G<id(Ol*dD>Tap$49(0_~_rbmt
s5t1wqpQU1;gl2cL(c$?2Vlz|y3vYW%ba;w#qW}N^07*qoM6N<$g0}PD7ytkO

literal 0
HcmV?d00001

diff --git a/packages/frontend/src/components/MkCaptcha.vue b/packages/frontend/src/components/MkCaptcha.vue
index c5b6e0caed..82fc89e51c 100644
--- a/packages/frontend/src/components/MkCaptcha.vue
+++ b/packages/frontend/src/components/MkCaptcha.vue
@@ -10,6 +10,17 @@ SPDX-License-Identifier: AGPL-3.0-only
 		<div id="mcaptcha__widget-container" class="m-captcha-style"></div>
 		<div ref="captchaEl"></div>
 	</div>
+	<div v-if="props.provider == 'testcaptcha'" style="background: #eee; border: solid 1px #888; padding: 8px; color: #000; max-width: 320px; display: flex; gap: 10px; align-items: center; box-shadow: 2px 2px 6px #0004; border-radius: 4px;">
+		<img src="/client-assets/testcaptcha.png" style="width: 60px; height: 60px; "/>
+		<div v-if="testcaptchaPassed">
+			<div style="color: green;">Test captcha passed!</div>
+		</div>
+		<div v-else>
+			<div style="font-size: 13px; margin-bottom: 4px;">Type "ai-chan-kawaii" to pass captcha</div>
+			<input v-model="testcaptchaInput" data-cy-testcaptcha-input/>
+			<button type="button" data-cy-testcaptcha-submit @click="testcaptchaSubmit">Submit</button>
+		</div>
+	</div>
 	<div v-else ref="captchaEl"></div>
 </div>
 </template>
@@ -29,7 +40,7 @@ export type Captcha = {
 	getResponse(id: string): string;
 };
 
-export type CaptchaProvider = 'hcaptcha' | 'recaptcha' | 'turnstile' | 'mcaptcha';
+export type CaptchaProvider = 'hcaptcha' | 'recaptcha' | 'turnstile' | 'mcaptcha' | 'testcaptcha';
 
 type CaptchaContainer = {
 	readonly [_ in CaptchaProvider]?: Captcha;
@@ -54,12 +65,16 @@ const available = ref(false);
 
 const captchaEl = shallowRef<HTMLDivElement | undefined>();
 
+const testcaptchaInput = ref('');
+const testcaptchaPassed = ref(false);
+
 const variable = computed(() => {
 	switch (props.provider) {
 		case 'hcaptcha': return 'hcaptcha';
 		case 'recaptcha': return 'grecaptcha';
 		case 'turnstile': return 'turnstile';
 		case 'mcaptcha': return 'mcaptcha';
+		case 'testcaptcha': return 'testcaptcha';
 	}
 });
 
@@ -71,6 +86,7 @@ const src = computed(() => {
 		case 'recaptcha': return 'https://www.recaptcha.net/recaptcha/api.js?render=explicit';
 		case 'turnstile': return 'https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit';
 		case 'mcaptcha': return null;
+		case 'testcaptcha': return null;
 	}
 });
 
@@ -78,7 +94,7 @@ const scriptId = computed(() => `script-${props.provider}`);
 
 const captcha = computed<Captcha>(() => window[variable.value] || {} as unknown as Captcha);
 
-if (loaded || props.provider === 'mcaptcha') {
+if (loaded || props.provider === 'mcaptcha' || props.provider === 'testcaptcha') {
 	available.value = true;
 } else if (src.value !== null) {
 	(document.getElementById(scriptId.value) ?? document.head.appendChild(Object.assign(document.createElement('script'), {
@@ -91,6 +107,8 @@ if (loaded || props.provider === 'mcaptcha') {
 
 function reset() {
 	if (captcha.value.reset) captcha.value.reset();
+	testcaptchaPassed.value = false;
+	testcaptchaInput.value = '';
 }
 
 async function requestRender() {
@@ -127,6 +145,12 @@ function onReceivedMessage(message: MessageEvent) {
 	}
 }
 
+function testcaptchaSubmit() {
+	testcaptchaPassed.value = testcaptchaInput.value === 'ai-chan-kawaii';
+	callback(testcaptchaPassed.value ? 'testcaptcha-passed' : undefined);
+	if (!testcaptchaPassed.value) testcaptchaInput.value = '';
+}
+
 onMounted(() => {
 	if (available.value) {
 		window.addEventListener('message', onReceivedMessage);
diff --git a/packages/frontend/src/components/MkSignin.password.vue b/packages/frontend/src/components/MkSignin.password.vue
index f30bf5f861..5608122a39 100644
--- a/packages/frontend/src/components/MkSignin.password.vue
+++ b/packages/frontend/src/components/MkSignin.password.vue
@@ -28,6 +28,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 				<MkCaptcha v-if="instance.enableMcaptcha" ref="mcaptcha" v-model="mCaptchaResponse" :class="$style.captcha" provider="mcaptcha" :sitekey="instance.mcaptchaSiteKey" :instanceUrl="instance.mcaptchaInstanceUrl"/>
 				<MkCaptcha v-if="instance.enableRecaptcha" ref="recaptcha" v-model="reCaptchaResponse" :class="$style.captcha" provider="recaptcha" :sitekey="instance.recaptchaSiteKey"/>
 				<MkCaptcha v-if="instance.enableTurnstile" ref="turnstile" v-model="turnstileResponse" :class="$style.captcha" provider="turnstile" :sitekey="instance.turnstileSiteKey"/>
+				<MkCaptcha v-if="instance.enableTestcaptcha" ref="testcaptcha" v-model="testcaptchaResponse" :class="$style.captcha" provider="testcaptcha"/>
 			</div>
 
 			<MkButton type="submit" :disabled="needCaptcha && captchaFailed" large primary rounded style="margin: 0 auto;" data-cy-signin-page-password-continue>{{ i18n.ts.continue }} <i class="ti ti-arrow-right"></i></MkButton>
@@ -44,6 +45,7 @@ export type PwResponse = {
 		mCaptchaResponse: string | null;
 		reCaptchaResponse: string | null;
 		turnstileResponse: string | null;
+		testcaptchaResponse: string | null;
 	};
 };
 </script>
@@ -75,18 +77,21 @@ const hCaptcha = useTemplateRef('hcaptcha');
 const mCaptcha = useTemplateRef('mcaptcha');
 const reCaptcha = useTemplateRef('recaptcha');
 const turnstile = useTemplateRef('turnstile');
+const testcaptcha = useTemplateRef('testcaptcha');
 
 const hCaptchaResponse = ref<string | null>(null);
 const mCaptchaResponse = ref<string | null>(null);
 const reCaptchaResponse = ref<string | null>(null);
 const turnstileResponse = ref<string | null>(null);
+const testcaptchaResponse = ref<string | null>(null);
 
 const captchaFailed = computed((): boolean => {
 	return (
 		(instance.enableHcaptcha && !hCaptchaResponse.value) ||
 		(instance.enableMcaptcha && !mCaptchaResponse.value) ||
 		(instance.enableRecaptcha && !reCaptchaResponse.value) ||
-		(instance.enableTurnstile && !turnstileResponse.value)
+		(instance.enableTurnstile && !turnstileResponse.value) ||
+		(instance.enableTestcaptcha && !testcaptchaResponse.value)
 	);
 });
 
@@ -104,6 +109,7 @@ function onSubmit() {
 			mCaptchaResponse: mCaptchaResponse.value,
 			reCaptchaResponse: reCaptchaResponse.value,
 			turnstileResponse: turnstileResponse.value,
+			testcaptchaResponse: testcaptchaResponse.value,
 		},
 	});
 }
@@ -113,6 +119,7 @@ function resetCaptcha() {
 	mCaptcha.value?.reset();
 	reCaptcha.value?.reset();
 	turnstile.value?.reset();
+	testcaptcha.value?.reset();
 }
 
 defineExpose({
diff --git a/packages/frontend/src/components/MkSignin.vue b/packages/frontend/src/components/MkSignin.vue
index a773cefdab..776ee20e36 100644
--- a/packages/frontend/src/components/MkSignin.vue
+++ b/packages/frontend/src/components/MkSignin.vue
@@ -68,6 +68,8 @@ import { nextTick, onBeforeUnmount, ref, shallowRef, useTemplateRef } from 'vue'
 import * as Misskey from 'misskey-js';
 import { supported as webAuthnSupported, parseRequestOptionsFromJSON } from '@github/webauthn-json/browser-ponyfill';
 
+import type { AuthenticationPublicKeyCredential } from '@github/webauthn-json/browser-ponyfill';
+import type { OpenOnRemoteOptions } from '@/scripts/please-login.js';
 import { misskeyApi } from '@/scripts/misskey-api.js';
 import { showSuspendedDialog } from '@/scripts/show-suspended-dialog.js';
 import { login } from '@/account.js';
@@ -79,9 +81,6 @@ import XPassword, { type PwResponse } from '@/components/MkSignin.password.vue';
 import XTotp from '@/components/MkSignin.totp.vue';
 import XPasskey from '@/components/MkSignin.passkey.vue';
 
-import type { AuthenticationPublicKeyCredential } from '@github/webauthn-json/browser-ponyfill';
-import type { OpenOnRemoteOptions } from '@/scripts/please-login.js';
-
 const emit = defineEmits<{
 	(ev: 'login', v: Misskey.entities.SigninFlowResponse & { finished: true }): void;
 }>();
@@ -188,6 +187,7 @@ async function onPasswordSubmitted(pw: PwResponse) {
 			'm-captcha-response': pw.captcha.mCaptchaResponse,
 			'g-recaptcha-response': pw.captcha.reCaptchaResponse,
 			'turnstile-response': pw.captcha.turnstileResponse,
+			'testcaptcha-response': pw.captcha.testcaptchaResponse,
 		});
 	}
 }
diff --git a/packages/frontend/src/components/MkSignupDialog.form.vue b/packages/frontend/src/components/MkSignupDialog.form.vue
index ffb5551ff3..3d1c44fc90 100644
--- a/packages/frontend/src/components/MkSignupDialog.form.vue
+++ b/packages/frontend/src/components/MkSignupDialog.form.vue
@@ -66,6 +66,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 			<MkCaptcha v-if="instance.enableMcaptcha" ref="mcaptcha" v-model="mCaptchaResponse" :class="$style.captcha" provider="mcaptcha" :sitekey="instance.mcaptchaSiteKey" :instanceUrl="instance.mcaptchaInstanceUrl"/>
 			<MkCaptcha v-if="instance.enableRecaptcha" ref="recaptcha" v-model="reCaptchaResponse" :class="$style.captcha" provider="recaptcha" :sitekey="instance.recaptchaSiteKey"/>
 			<MkCaptcha v-if="instance.enableTurnstile" ref="turnstile" v-model="turnstileResponse" :class="$style.captcha" provider="turnstile" :sitekey="instance.turnstileSiteKey"/>
+			<MkCaptcha v-if="instance.enableTestcaptcha" ref="testcaptcha" v-model="testcaptchaResponse" :class="$style.captcha" provider="testcaptcha"/>
 			<MkButton type="submit" :disabled="shouldDisableSubmitting" large gradate rounded data-cy-signup-submit style="margin: 0 auto;">
 				<template v-if="submitting">
 					<MkLoading :em="true" :colored="false"/>
@@ -108,6 +109,7 @@ const hcaptcha = ref<Captcha | undefined>();
 const mcaptcha = ref<Captcha | undefined>();
 const recaptcha = ref<Captcha | undefined>();
 const turnstile = ref<Captcha | undefined>();
+const testcaptcha = ref<Captcha | undefined>();
 
 const username = ref<string>('');
 const password = ref<string>('');
@@ -123,6 +125,7 @@ const hCaptchaResponse = ref<string | null>(null);
 const mCaptchaResponse = ref<string | null>(null);
 const reCaptchaResponse = ref<string | null>(null);
 const turnstileResponse = ref<string | null>(null);
+const testcaptchaResponse = ref<string | null>(null);
 const usernameAbortController = ref<null | AbortController>(null);
 const emailAbortController = ref<null | AbortController>(null);
 
@@ -132,6 +135,7 @@ const shouldDisableSubmitting = computed((): boolean => {
 		instance.enableMcaptcha && !mCaptchaResponse.value ||
 		instance.enableRecaptcha && !reCaptchaResponse.value ||
 		instance.enableTurnstile && !turnstileResponse.value ||
+		instance.enableTestcaptcha && !testcaptchaResponse.value ||
 		instance.emailRequiredForSignup && emailState.value !== 'ok' ||
 		usernameState.value !== 'ok' ||
 		passwordRetypeState.value !== 'match';
@@ -259,6 +263,7 @@ async function onSubmit(): Promise<void> {
 		'm-captcha-response': mCaptchaResponse.value,
 		'g-recaptcha-response': reCaptchaResponse.value,
 		'turnstile-response': turnstileResponse.value,
+		'testcaptcha-response': testcaptchaResponse.value,
 	};
 
 	const res = await fetch(`${config.apiUrl}/signup`, {
@@ -301,6 +306,7 @@ function onSignupApiError() {
 	mcaptcha.value?.reset?.();
 	recaptcha.value?.reset?.();
 	turnstile.value?.reset?.();
+	testcaptcha.value?.reset?.();
 
 	os.alert({
 		type: 'error',
diff --git a/packages/frontend/src/pages/admin/bot-protection.vue b/packages/frontend/src/pages/admin/bot-protection.vue
index b34592cd6a..d07add4408 100644
--- a/packages/frontend/src/pages/admin/bot-protection.vue
+++ b/packages/frontend/src/pages/admin/bot-protection.vue
@@ -11,6 +11,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 	<template v-else-if="botProtectionForm.savedState.provider === 'mcaptcha'" #suffix>mCaptcha</template>
 	<template v-else-if="botProtectionForm.savedState.provider === 'recaptcha'" #suffix>reCAPTCHA</template>
 	<template v-else-if="botProtectionForm.savedState.provider === 'turnstile'" #suffix>Turnstile</template>
+	<template v-else-if="botProtectionForm.savedState.provider === 'testcaptcha'" #suffix>testCaptcha</template>
 	<template v-else #suffix>{{ i18n.ts.none }} ({{ i18n.ts.notRecommended }})</template>
 	<template v-if="botProtectionForm.modified.value" #footer>
 		<MkFormFooter :form="botProtectionForm"/>
@@ -23,6 +24,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 			<option value="mcaptcha">mCaptcha</option>
 			<option value="recaptcha">reCAPTCHA</option>
 			<option value="turnstile">Turnstile</option>
+			<option value="testcaptcha">testCaptcha</option>
 		</MkRadios>
 
 		<template v-if="botProtectionForm.state.provider === 'hcaptcha'">
@@ -85,6 +87,13 @@ SPDX-License-Identifier: AGPL-3.0-only
 				<MkCaptcha provider="turnstile" :sitekey="botProtectionForm.state.turnstileSiteKey || '1x00000000000000000000AA'"/>
 			</FormSlot>
 		</template>
+		<template v-else-if="botProtectionForm.state.provider === 'testcaptcha'">
+			<MkInfo warn><span v-html="i18n.ts.testCaptchaWarning"></span></MkInfo>
+			<FormSlot>
+				<template #label>{{ i18n.ts.preview }}</template>
+				<MkCaptcha provider="testcaptcha"/>
+			</FormSlot>
+		</template>
 	</div>
 </MkFolder>
 </template>
@@ -101,6 +110,7 @@ import { i18n } from '@/i18n.js';
 import { useForm } from '@/scripts/use-form.js';
 import MkFormFooter from '@/components/MkFormFooter.vue';
 import MkFolder from '@/components/MkFolder.vue';
+import MkInfo from '@/components/MkInfo.vue';
 
 const MkCaptcha = defineAsyncComponent(() => import('@/components/MkCaptcha.vue'));
 
@@ -115,7 +125,9 @@ const botProtectionForm = useForm({
 				? 'turnstile'
 				: meta.enableMcaptcha
 					? 'mcaptcha'
-					: null,
+					: meta.enableTestcaptcha
+						? 'testcaptcha'
+						: null,
 	hcaptchaSiteKey: meta.hcaptchaSiteKey,
 	hcaptchaSecretKey: meta.hcaptchaSecretKey,
 	mcaptchaSiteKey: meta.mcaptchaSiteKey,
@@ -140,6 +152,7 @@ const botProtectionForm = useForm({
 		enableTurnstile: state.provider === 'turnstile',
 		turnstileSiteKey: state.turnstileSiteKey,
 		turnstileSecretKey: state.turnstileSecretKey,
+		enableTestcaptcha: state.provider === 'testcaptcha',
 	});
 	fetchInstance(true);
 });
diff --git a/packages/misskey-js/src/autogen/types.ts b/packages/misskey-js/src/autogen/types.ts
index 76ef7ea1fb..e40cb050fd 100644
--- a/packages/misskey-js/src/autogen/types.ts
+++ b/packages/misskey-js/src/autogen/types.ts
@@ -4972,6 +4972,7 @@ export type components = {
       recaptchaSiteKey: string | null;
       enableTurnstile: boolean;
       turnstileSiteKey: string | null;
+      enableTestcaptcha: boolean;
       swPublickey: string | null;
       /** @default /assets/ai.png */
       mascotImageUrl: string;
@@ -5102,6 +5103,7 @@ export type operations = {
             recaptchaSiteKey: string | null;
             enableTurnstile: boolean;
             turnstileSiteKey: string | null;
+            enableTestcaptcha: boolean;
             swPublickey: string | null;
             /** @default /assets/ai.png */
             mascotImageUrl: string | null;
@@ -9491,6 +9493,7 @@ export type operations = {
           enableTurnstile?: boolean;
           turnstileSiteKey?: string | null;
           turnstileSecretKey?: string | null;
+          enableTestcaptcha?: boolean;
           /** @enum {string} */
           sensitiveMediaDetection?: 'none' | 'all' | 'local' | 'remote';
           /** @enum {string} */

From 777804605eb056c6f139ad07827413467b7cee9a Mon Sep 17 00:00:00 2001
From: "github-actions[bot]" <github-actions[bot]@users.noreply.github.com>
Date: Fri, 11 Oct 2024 12:13:47 +0000
Subject: [PATCH 086/121] Bump version to 2024.10.1-beta.3

---
 package.json                     | 2 +-
 packages/misskey-js/package.json | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/package.json b/package.json
index 31d46d79f8..2c84c55303 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
 	"name": "misskey",
-	"version": "2024.10.1-beta.2",
+	"version": "2024.10.1-beta.3",
 	"codename": "nasubi",
 	"repository": {
 		"type": "git",
diff --git a/packages/misskey-js/package.json b/packages/misskey-js/package.json
index 268eab7978..e5f4b7b9dd 100644
--- a/packages/misskey-js/package.json
+++ b/packages/misskey-js/package.json
@@ -1,7 +1,7 @@
 {
 	"type": "module",
 	"name": "misskey-js",
-	"version": "2024.10.1-beta.2",
+	"version": "2024.10.1-beta.3",
 	"description": "Misskey SDK for JavaScript",
 	"license": "MIT",
 	"main": "./built/index.js",

From 2f09d69773dcc6c4607b2c9e1f5c86cc1337ece1 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=8B=E3=81=A3=E3=81=93=E3=81=8B=E3=82=8A?=
 <67428053+kakkokari-gtyih@users.noreply.github.com>
Date: Fri, 11 Oct 2024 21:29:03 +0900
Subject: [PATCH 087/121] =?UTF-8?q?fix(backend):=20=E3=82=AD=E3=83=A5?=
 =?UTF-8?q?=E3=83=BC=E3=81=AE=E3=82=A8=E3=83=A9=E3=83=BC=E3=83=AD=E3=82=B0?=
 =?UTF-8?q?=E3=82=92=E7=B0=A1=E7=95=A5=E5=8C=96=E3=81=99=E3=82=8B=E3=82=88?=
 =?UTF-8?q?=E3=81=86=E3=81=AB=20(#14748)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

* reduce federation log spam

* Don't record stack trace for unrecoverable errors.
* Avoid logging duplicate stace traces.

(cherry picked from commit ed0570110bf8cb8e8959591dccfa3c35999106ce)

* improve error summaries

(cherry picked from commit 20dd66f735d9778df0371001e303549dce619260)

* fix lint errors

(cherry picked from commit 83869e1c470b12b3bf4b23d885514d926620662a)

* condense job info

(cherry picked from commit 786702e076ad1af14538849512ad31c0ced7afe6)

* fix maxAttempts calculation

(cherry picked from commit b4d10aa8f821e594ec9c907eb2a5bdb3c73c67d5)

* condense error info

(cherry picked from commit f62cd8941ced74a4865aa5eae4f4a1c7aa1d30f1)

* normalize ID logging

(cherry picked from commit d8e1e4890d28347239162e26235eb68b1ff96654)

* further condense error details

(cherry picked from commit d867c2089b3b24680df0713a2aa0914789e45670)

* collapse AbortErrors

(cherry picked from commit 5171ba7113ebc7242527768afb9ab4cec534e3b3)

* don't log job name unless it has one

(cherry picked from commit a5316c06ed770b60f7b4c7ff5aa8c71cc0558db7)

* Update Changelog

* Record origin

---------

Co-authored-by: Hazel K <acomputerdog@gmail.com>
---
 CHANGELOG.md                                  |  4 +
 .../src/queue/QueueProcessorService.ts        | 86 +++++++++++--------
 2 files changed, 52 insertions(+), 38 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 030dbfda28..143b63f7b2 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -12,6 +12,10 @@
 - Feat: モデレータ権限を持つユーザが全員7日間活動しなかった場合は自動的に招待制へと移行するように ( #13437 )
 - Fix: `admin/emoji/update`エンドポイントのidのみ指定した時不正なエラーが発生するバグを修正
 
+### Server
+- Fix: キューのエラーログを簡略化するように  
+  (Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/649)
+
 ## 2024.10.0
 
 ### Note
diff --git a/packages/backend/src/queue/QueueProcessorService.ts b/packages/backend/src/queue/QueueProcessorService.ts
index 85e148e900..6940e1c188 100644
--- a/packages/backend/src/queue/QueueProcessorService.ts
+++ b/packages/backend/src/queue/QueueProcessorService.ts
@@ -67,7 +67,7 @@ function getJobInfo(job: Bull.Job | undefined, increment = false): string {
 
 	// onActiveとかonCompletedのattemptsMadeがなぜか0始まりなのでインクリメントする
 	const currentAttempts = job.attemptsMade + (increment ? 1 : 0);
-	const maxAttempts = job.opts ? job.opts.attempts : 0;
+	const maxAttempts = job.opts.attempts ?? 0;
 
 	return `id=${job.id} attempts=${currentAttempts}/${maxAttempts} age=${formated}`;
 }
@@ -126,20 +126,30 @@ export class QueueProcessorService implements OnApplicationShutdown {
 	) {
 		this.logger = this.queueLoggerService.logger;
 
-		function renderError(e: Error): any {
-			if (e) { // 何故かeがundefinedで来ることがある
-				return {
-					stack: e.stack,
-					message: e.message,
-					name: e.name,
-				};
-			} else {
-				return {
-					stack: '?',
-					message: '?',
-					name: '?',
-				};
+		function renderError(e?: Error) {
+			// 何故かeがundefinedで来ることがある
+			if (!e) return '?';
+
+			if (e instanceof Bull.UnrecoverableError || e.name === 'AbortError') {
+				return `${e.name}: ${e.message}`;
 			}
+
+			return {
+				stack: e.stack,
+				message: e.message,
+				name: e.name,
+			};
+		}
+
+		function renderJob(job?: Bull.Job) {
+			if (!job) return '?';
+
+			return {
+				name: job.name || undefined,
+				info: getJobInfo(job),
+				failedReason: job.failedReason || undefined,
+				data: job.data,
+			};
 		}
 
 		//#region system
@@ -175,15 +185,15 @@ export class QueueProcessorService implements OnApplicationShutdown {
 				.on('active', (job) => logger.debug(`active id=${job.id}`))
 				.on('completed', (job, result) => logger.debug(`completed(${result}) id=${job.id}`))
 				.on('failed', (job, err: Error) => {
-					logger.error(`failed(${err.stack}) id=${job ? job.id : '-'}`, { job, e: renderError(err) });
+					logger.error(`failed(${err.name}: ${err.message}) id=${job?.id ?? '?'}`, { job: renderJob(job), e: renderError(err) });
 					if (config.sentryForBackend) {
-						Sentry.captureMessage(`Queue: System: ${job?.name ?? '?'}: ${err.message}`, {
+						Sentry.captureMessage(`Queue: System: ${job?.name ?? '?'}: ${err.name}: ${err.message}`, {
 							level: 'error',
 							extra: { job, err },
 						});
 					}
 				})
-				.on('error', (err: Error) => logger.error(`error ${err.stack}`, { e: renderError(err) }))
+				.on('error', (err: Error) => logger.error(`error ${err.name}: ${err.message}`, { e: renderError(err) }))
 				.on('stalled', (jobId) => logger.warn(`stalled id=${jobId}`));
 		}
 		//#endregion
@@ -232,15 +242,15 @@ export class QueueProcessorService implements OnApplicationShutdown {
 				.on('active', (job) => logger.debug(`active id=${job.id}`))
 				.on('completed', (job, result) => logger.debug(`completed(${result}) id=${job.id}`))
 				.on('failed', (job, err) => {
-					logger.error(`failed(${err.stack}) id=${job ? job.id : '-'}`, { job, e: renderError(err) });
+					logger.error(`failed(${err.name}: ${err.message}) id=${job?.id ?? '?'}`, { job: renderJob(job), e: renderError(err) });
 					if (config.sentryForBackend) {
-						Sentry.captureMessage(`Queue: DB: ${job?.name ?? '?'}: ${err.message}`, {
+						Sentry.captureMessage(`Queue: DB: ${job?.name ?? '?'}: ${err.name}: ${err.message}`, {
 							level: 'error',
 							extra: { job, err },
 						});
 					}
 				})
-				.on('error', (err: Error) => logger.error(`error ${err.stack}`, { e: renderError(err) }))
+				.on('error', (err: Error) => logger.error(`error ${err.name}: ${err.message}`, { e: renderError(err) }))
 				.on('stalled', (jobId) => logger.warn(`stalled id=${jobId}`));
 		}
 		//#endregion
@@ -272,15 +282,15 @@ export class QueueProcessorService implements OnApplicationShutdown {
 				.on('active', (job) => logger.debug(`active ${getJobInfo(job, true)} to=${job.data.to}`))
 				.on('completed', (job, result) => logger.debug(`completed(${result}) ${getJobInfo(job, true)} to=${job.data.to}`))
 				.on('failed', (job, err) => {
-					logger.error(`failed(${err.stack}) ${getJobInfo(job)} to=${job ? job.data.to : '-'}`);
+					logger.error(`failed(${err.name}: ${err.message}) ${getJobInfo(job)} to=${job ? job.data.to : '-'}`);
 					if (config.sentryForBackend) {
-						Sentry.captureMessage(`Queue: Deliver: ${err.message}`, {
+						Sentry.captureMessage(`Queue: Deliver: ${err.name}: ${err.message}`, {
 							level: 'error',
 							extra: { job, err },
 						});
 					}
 				})
-				.on('error', (err: Error) => logger.error(`error ${err.stack}`, { e: renderError(err) }))
+				.on('error', (err: Error) => logger.error(`error ${err.name}: ${err.message}`, { e: renderError(err) }))
 				.on('stalled', (jobId) => logger.warn(`stalled id=${jobId}`));
 		}
 		//#endregion
@@ -312,15 +322,15 @@ export class QueueProcessorService implements OnApplicationShutdown {
 				.on('active', (job) => logger.debug(`active ${getJobInfo(job, true)}`))
 				.on('completed', (job, result) => logger.debug(`completed(${result}) ${getJobInfo(job, true)}`))
 				.on('failed', (job, err) => {
-					logger.error(`failed(${err.stack}) ${getJobInfo(job)} activity=${job ? (job.data.activity ? job.data.activity.id : 'none') : '-'}`, { job, e: renderError(err) });
+					logger.error(`failed(${err.name}: ${err.message}) ${getJobInfo(job)} activity=${job ? (job.data.activity ? job.data.activity.id : 'none') : '-'}`, { job: renderJob(job), e: renderError(err) });
 					if (config.sentryForBackend) {
-						Sentry.captureMessage(`Queue: Inbox: ${err.message}`, {
+						Sentry.captureMessage(`Queue: Inbox: ${err.name}: ${err.message}`, {
 							level: 'error',
 							extra: { job, err },
 						});
 					}
 				})
-				.on('error', (err: Error) => logger.error(`error ${err.stack}`, { e: renderError(err) }))
+				.on('error', (err: Error) => logger.error(`error ${err.name}: ${err.message}`, { e: renderError(err) }))
 				.on('stalled', (jobId) => logger.warn(`stalled id=${jobId}`));
 		}
 		//#endregion
@@ -352,15 +362,15 @@ export class QueueProcessorService implements OnApplicationShutdown {
 				.on('active', (job) => logger.debug(`active ${getJobInfo(job, true)} to=${job.data.to}`))
 				.on('completed', (job, result) => logger.debug(`completed(${result}) ${getJobInfo(job, true)} to=${job.data.to}`))
 				.on('failed', (job, err) => {
-					logger.error(`failed(${err.stack}) ${getJobInfo(job)} to=${job ? job.data.to : '-'}`);
+					logger.error(`failed(${err.name}: ${err.message}) ${getJobInfo(job)} to=${job ? job.data.to : '-'}`);
 					if (config.sentryForBackend) {
-						Sentry.captureMessage(`Queue: UserWebhookDeliver: ${err.message}`, {
+						Sentry.captureMessage(`Queue: UserWebhookDeliver: ${err.name}: ${err.message}`, {
 							level: 'error',
 							extra: { job, err },
 						});
 					}
 				})
-				.on('error', (err: Error) => logger.error(`error ${err.stack}`, { e: renderError(err) }))
+				.on('error', (err: Error) => logger.error(`error ${err.name}: ${err.message}`, { e: renderError(err) }))
 				.on('stalled', (jobId) => logger.warn(`stalled id=${jobId}`));
 		}
 		//#endregion
@@ -392,15 +402,15 @@ export class QueueProcessorService implements OnApplicationShutdown {
 				.on('active', (job) => logger.debug(`active ${getJobInfo(job, true)} to=${job.data.to}`))
 				.on('completed', (job, result) => logger.debug(`completed(${result}) ${getJobInfo(job, true)} to=${job.data.to}`))
 				.on('failed', (job, err) => {
-					logger.error(`failed(${err.stack}) ${getJobInfo(job)} to=${job ? job.data.to : '-'}`);
+					logger.error(`failed(${err.name}: ${err.message}) ${getJobInfo(job)} to=${job ? job.data.to : '-'}`);
 					if (config.sentryForBackend) {
-						Sentry.captureMessage(`Queue: SystemWebhookDeliver: ${err.message}`, {
+						Sentry.captureMessage(`Queue: SystemWebhookDeliver: ${err.name}: ${err.message}`, {
 							level: 'error',
 							extra: { job, err },
 						});
 					}
 				})
-				.on('error', (err: Error) => logger.error(`error ${err.stack}`, { e: renderError(err) }))
+				.on('error', (err: Error) => logger.error(`error ${err.name}: ${err.message}`, { e: renderError(err) }))
 				.on('stalled', (jobId) => logger.warn(`stalled id=${jobId}`));
 		}
 		//#endregion
@@ -439,15 +449,15 @@ export class QueueProcessorService implements OnApplicationShutdown {
 				.on('active', (job) => logger.debug(`active id=${job.id}`))
 				.on('completed', (job, result) => logger.debug(`completed(${result}) id=${job.id}`))
 				.on('failed', (job, err) => {
-					logger.error(`failed(${err.stack}) id=${job ? job.id : '-'}`, { job, e: renderError(err) });
+					logger.error(`failed(${err.name}: ${err.message}) id=${job?.id ?? '?'}`, { job: renderJob(job), e: renderError(err) });
 					if (config.sentryForBackend) {
-						Sentry.captureMessage(`Queue: Relationship: ${job?.name ?? '?'}: ${err.message}`, {
+						Sentry.captureMessage(`Queue: Relationship: ${job?.name ?? '?'}: ${err.name}: ${err.message}`, {
 							level: 'error',
 							extra: { job, err },
 						});
 					}
 				})
-				.on('error', (err: Error) => logger.error(`error ${err.stack}`, { e: renderError(err) }))
+				.on('error', (err: Error) => logger.error(`error ${err.name}: ${err.message}`, { e: renderError(err) }))
 				.on('stalled', (jobId) => logger.warn(`stalled id=${jobId}`));
 		}
 		//#endregion
@@ -480,15 +490,15 @@ export class QueueProcessorService implements OnApplicationShutdown {
 				.on('active', (job) => logger.debug(`active id=${job.id}`))
 				.on('completed', (job, result) => logger.debug(`completed(${result}) id=${job.id}`))
 				.on('failed', (job, err) => {
-					logger.error(`failed(${err.stack}) id=${job ? job.id : '-'}`, { job, e: renderError(err) });
+					logger.error(`failed(${err.name}: ${err.message}) id=${job?.id ?? '?'}`, { job: renderJob(job), e: renderError(err) });
 					if (config.sentryForBackend) {
-						Sentry.captureMessage(`Queue: ObjectStorage: ${job?.name ?? '?'}: ${err.message}`, {
+						Sentry.captureMessage(`Queue: ObjectStorage: ${job?.name ?? '?'}: ${err.name}: ${err.message}`, {
 							level: 'error',
 							extra: { job, err },
 						});
 					}
 				})
-				.on('error', (err: Error) => logger.error(`error ${err.stack}`, { e: renderError(err) }))
+				.on('error', (err: Error) => logger.error(`error ${err.name}: ${err.message}`, { e: renderError(err) }))
 				.on('stalled', (jobId) => logger.warn(`stalled id=${jobId}`));
 		}
 		//#endregion

From a87a18f40d6b8c8ff44a0bccc7fadb34029f3812 Mon Sep 17 00:00:00 2001
From: syuilo <4439005+syuilo@users.noreply.github.com>
Date: Sat, 12 Oct 2024 10:11:55 +0900
Subject: [PATCH 088/121] Update about-misskey.vue

---
 packages/frontend/src/pages/about-misskey.vue | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/packages/frontend/src/pages/about-misskey.vue b/packages/frontend/src/pages/about-misskey.vue
index 891489f1a1..68b98c2ab7 100644
--- a/packages/frontend/src/pages/about-misskey.vue
+++ b/packages/frontend/src/pages/about-misskey.vue
@@ -266,6 +266,9 @@ const patronsWithIcon = [{
 }, {
 	name: 'なっかあ',
 	icon: 'https://assets.misskey-hub.net/patrons/c2f5f3e394e74a64912284a2f4ca710e.jpg',
+}, {
+	name: '如月ユカ',
+	icon: 'https://assets.misskey-hub.net/patrons/f24a042076a041b6811a2f124eb620ca.jpg',
 }];
 
 const patrons = [

From ef90f83917c61afef607c67b16adbabea12b78a2 Mon Sep 17 00:00:00 2001
From: syuilo <4439005+syuilo@users.noreply.github.com>
Date: Sat, 12 Oct 2024 10:31:40 +0900
Subject: [PATCH 089/121] Update index.d.ts

---
 locales/index.d.ts | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/locales/index.d.ts b/locales/index.d.ts
index dab8eb0361..2dca73bfa6 100644
--- a/locales/index.d.ts
+++ b/locales/index.d.ts
@@ -5700,6 +5700,10 @@ export interface Locale extends ILocale {
          * サーバー運営者へのお問い合わせフォームのURLや、運営者の連絡先等が記載されたWebページのURLを指定します。
          */
         "inquiryUrlDescription": string;
+        /**
+         * 一定期間モデレーターのアクティビティが検出されなかった場合、スパム防止のためこの設定は自動でオフになります。
+         */
+        "thisSettingWillAutomaticallyOffWhenModeratorsInactive": string;
     };
     "_accountMigration": {
         /**

From 824c51a19f8cf3f4b6ad9bff56a5449d1b216ba1 Mon Sep 17 00:00:00 2001
From: syuilo <4439005+syuilo@users.noreply.github.com>
Date: Sat, 12 Oct 2024 10:32:00 +0900
Subject: [PATCH 090/121] fix(frontend): fix style

Fix #14754
---
 packages/frontend/src/components/MkWindow.vue | 6 +++++-
 1 file changed, 5 insertions(+), 1 deletion(-)

diff --git a/packages/frontend/src/components/MkWindow.vue b/packages/frontend/src/components/MkWindow.vue
index a5f7a2e9e5..056b6a37ed 100644
--- a/packages/frontend/src/components/MkWindow.vue
+++ b/packages/frontend/src/components/MkWindow.vue
@@ -54,9 +54,9 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <script lang="ts" setup>
 import { onBeforeUnmount, onMounted, provide, shallowRef, ref } from 'vue';
+import type { MenuItem } from '@/types/menu.js';
 import contains from '@/scripts/contains.js';
 import * as os from '@/os.js';
-import type { MenuItem } from '@/types/menu.js';
 import { i18n } from '@/i18n.js';
 import { defaultStore } from '@/store.js';
 
@@ -484,6 +484,10 @@ defineExpose({
 }
 
 .root {
+	// universal.vueとかで直接--MI-stickyBottomが定義されていたりするのでリセット
+	--MI-stickyTop: 0;
+	--MI-stickyBottom: 0;
+
 	position: fixed;
 	top: 0;
 	left: 0;

From 85bb1ff1db5f2156b88a588477efc137323e1333 Mon Sep 17 00:00:00 2001
From: syuilo <4439005+syuilo@users.noreply.github.com>
Date: Sat, 12 Oct 2024 11:18:26 +0900
Subject: [PATCH 091/121] :art:

---
 packages/frontend/src/components/global/MkAd.vue | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/packages/frontend/src/components/global/MkAd.vue b/packages/frontend/src/components/global/MkAd.vue
index 646304fb06..1eded847ef 100644
--- a/packages/frontend/src/components/global/MkAd.vue
+++ b/packages/frontend/src/components/global/MkAd.vue
@@ -124,7 +124,7 @@ function reduceFrequency(): void {
 <style lang="scss" module>
 .root {
 	background-size: auto auto;
-	background-image: repeating-linear-gradient(45deg, transparent, transparent 8px, var(--bg) 8px, var(--bg) 14px );
+	background-image: repeating-linear-gradient(45deg, transparent, transparent 8px, var(--MI_THEME-bg) 8px, var(--MI_THEME-bg) 14px );
 }
 
 .main {

From ee08e9f51e5079a0a1ba1ff2f109ae72c3f19dd7 Mon Sep 17 00:00:00 2001
From: FineArchs <133759614+FineArchs@users.noreply.github.com>
Date: Sat, 12 Oct 2024 11:20:55 +0900
Subject: [PATCH 092/121] =?UTF-8?q?refactor:=20MkStickyContainer=E3=81=A7<?=
 =?UTF-8?q?style=20/>=E3=82=92=E4=BD=BF=E3=81=86=20(#14755)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

* remove rootEL ref

* use css module

* use v-bind in css

* --MI prefix

* remove unused ref

---------

Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com>
---
 .../components/global/MkStickyContainer.vue   | 56 +++++++++----------
 1 file changed, 25 insertions(+), 31 deletions(-)

diff --git a/packages/frontend/src/components/global/MkStickyContainer.vue b/packages/frontend/src/components/global/MkStickyContainer.vue
index cb21dafd2b..2763ecadd6 100644
--- a/packages/frontend/src/components/global/MkStickyContainer.vue
+++ b/packages/frontend/src/components/global/MkStickyContainer.vue
@@ -4,19 +4,18 @@ SPDX-License-Identifier: AGPL-3.0-only
 -->
 
 <template>
-<div ref="rootEl">
-	<div ref="headerEl">
+<div>
+	<div ref="headerEl" :class="$style.header">
 		<slot name="header"></slot>
 	</div>
 	<div
-		ref="bodyEl"
+		:class="$style.body"
 		:data-sticky-container-header-height="headerHeight"
 		:data-sticky-container-footer-height="footerHeight"
-		style="position: relative; z-index: 0;"
 	>
 		<slot></slot>
 	</div>
-	<div ref="footerEl">
+	<div ref="footerEl" :class="$style.footer">
 		<slot name="footer"></slot>
 	</div>
 </div>
@@ -27,10 +26,8 @@ import { onMounted, onUnmounted, provide, inject, Ref, ref, watch, shallowRef }
 
 import { CURRENT_STICKY_BOTTOM, CURRENT_STICKY_TOP } from '@@/js/const.js';
 
-const rootEl = shallowRef<HTMLElement>();
 const headerEl = shallowRef<HTMLElement>();
 const footerEl = shallowRef<HTMLElement>();
-const bodyEl = shallowRef<HTMLElement>();
 
 const headerHeight = ref<string | undefined>();
 const childStickyTop = ref(0);
@@ -67,31 +64,11 @@ onMounted(() => {
 
 	watch([parentStickyTop, parentStickyBottom], calc);
 
-	watch(childStickyTop, () => {
-		if (bodyEl.value == null) return;
-		bodyEl.value.style.setProperty('--MI-stickyTop', `${childStickyTop.value}px`);
-	}, {
-		immediate: true,
-	});
-
-	watch(childStickyBottom, () => {
-		if (bodyEl.value == null) return;
-		bodyEl.value.style.setProperty('--MI-stickyBottom', `${childStickyBottom.value}px`);
-	}, {
-		immediate: true,
-	});
-
 	if (headerEl.value != null) {
-		headerEl.value.style.position = 'sticky';
-		headerEl.value.style.top = 'var(--MI-stickyTop, 0)';
-		headerEl.value.style.zIndex = '1';
 		observer.observe(headerEl.value);
 	}
 
 	if (footerEl.value != null) {
-		footerEl.value.style.position = 'sticky';
-		footerEl.value.style.bottom = 'var(--MI-stickyBottom, 0)';
-		footerEl.value.style.zIndex = '1';
 		observer.observe(footerEl.value);
 	}
 });
@@ -99,8 +76,25 @@ onMounted(() => {
 onUnmounted(() => {
 	observer.disconnect();
 });
-
-defineExpose({
-	rootEl: rootEl,
-});
 </script>
+
+<style lang='scss' module>
+.body {
+	position: relative;
+	z-index: 0;
+	--MI-stickyTop: v-bind("childStickyTop + 'px'");
+	--MI-stickyBottom: v-bind("childStickyBottom + 'px'");
+}
+
+.header {
+	position: sticky;
+	top: var(--MI-stickyTop, 0);
+	z-index: 1;
+}
+
+.footer {
+	position: sticky;
+	bottom: var(--MI-stickyBottom, 0);
+	z-index: 1;
+}
+</style>

From c4c69cd267012158d456be0852e9e51e62874848 Mon Sep 17 00:00:00 2001
From: syuilo <4439005+syuilo@users.noreply.github.com>
Date: Sat, 12 Oct 2024 11:28:58 +0900
Subject: [PATCH 093/121] :art:

---
 .../src/components/MkDateSeparatedList.vue     |  6 ++++--
 .../frontend/src/components/global/MkAd.vue    | 18 ++++++------------
 2 files changed, 10 insertions(+), 14 deletions(-)

diff --git a/packages/frontend/src/components/MkDateSeparatedList.vue b/packages/frontend/src/components/MkDateSeparatedList.vue
index a8a32e8bc7..5976aa02f5 100644
--- a/packages/frontend/src/components/MkDateSeparatedList.vue
+++ b/packages/frontend/src/components/MkDateSeparatedList.vue
@@ -100,10 +100,12 @@ export default defineComponent({
 				return [el, separator];
 			} else {
 				if (props.ad && item._shouldInsertAd_) {
-					return [h(MkAd, {
+					return [h('div', {
 						key: item.id + ':ad',
+						style: 'padding: 8px; background-size: auto auto; background-image: repeating-linear-gradient(45deg, transparent, transparent 8px, var(--MI_THEME-bg) 8px, var(--MI_THEME-bg) 14px );',
+					}, [h(MkAd, {
 						prefer: ['horizontal', 'horizontal-big'],
-					}), el];
+					})]), el];
 				} else {
 					return el;
 				}
diff --git a/packages/frontend/src/components/global/MkAd.vue b/packages/frontend/src/components/global/MkAd.vue
index 1eded847ef..792a087148 100644
--- a/packages/frontend/src/components/global/MkAd.vue
+++ b/packages/frontend/src/components/global/MkAd.vue
@@ -30,12 +30,10 @@ SPDX-License-Identifier: AGPL-3.0-only
 		</component>
 	</div>
 	<div v-else :class="$style.menu">
-		<div :class="$style.menuContainer">
-			<div>Ads by {{ host }}</div>
-			<!--<MkButton class="button" primary>{{ i18n.ts._ad.like }}</MkButton>-->
-			<MkButton v-if="chosen.ratio !== 0" :class="$style.menuButton" @click="reduceFrequency">{{ i18n.ts._ad.reduceFrequencyOfThisAd }}</MkButton>
-			<button class="_textButton" @click="toggleMenu">{{ i18n.ts._ad.back }}</button>
-		</div>
+		<div>Ads by {{ host }}</div>
+		<!--<MkButton class="button" primary>{{ i18n.ts._ad.like }}</MkButton>-->
+		<MkButton v-if="chosen.ratio !== 0" :class="$style.menuButton" @click="reduceFrequency">{{ i18n.ts._ad.reduceFrequencyOfThisAd }}</MkButton>
+		<button class="_textButton" @click="toggleMenu">{{ i18n.ts._ad.back }}</button>
 	</div>
 </div>
 <div v-else></div>
@@ -123,8 +121,7 @@ function reduceFrequency(): void {
 
 <style lang="scss" module>
 .root {
-	background-size: auto auto;
-	background-image: repeating-linear-gradient(45deg, transparent, transparent 8px, var(--MI_THEME-bg) 8px, var(--MI_THEME-bg) 14px );
+
 }
 
 .main {
@@ -202,14 +199,11 @@ function reduceFrequency(): void {
 }
 
 .menu {
-	padding: 8px;
 	text-align: center;
-}
-
-.menuContainer {
 	padding: 8px;
 	margin: 0 auto;
 	max-width: 400px;
+	background: var(--MI_THEME-panel);
 	border: solid 1px var(--MI_THEME-divider);
 }
 

From 45d42b8641585cbe582e4c2a95e03ef511df00be Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=8B=E3=81=A3=E3=81=93=E3=81=8B=E3=82=8A?=
 <67428053+kakkokari-gtyih@users.noreply.github.com>
Date: Sun, 13 Oct 2024 20:21:25 +0900
Subject: [PATCH 094/121] =?UTF-8?q?feat:=20=E3=83=A6=E3=83=BC=E3=82=B6?=
 =?UTF-8?q?=E3=83=BC=E3=81=AE=E5=90=8D=E5=89=8D=E3=81=AB=E7=A6=81=E6=AD=A2?=
 =?UTF-8?q?=E3=83=AF=E3=83=BC=E3=83=89=E3=82=92=E8=A8=AD=E5=AE=9A=E3=81=A7?=
 =?UTF-8?q?=E3=81=8D=E3=82=8B=E3=82=88=E3=81=86=E3=81=AB=20(#14756)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

* wip

* :art:

* Enhance: モデレーター以上は制限の影響を受けないように

* refactor

* better error handling

* fix

* Revert "better error handling"

This reverts commit 5670b29cfa18a3894d0c2abfe0e5ef862e3b9ffa.

* error handling

* エラーが出ないのを修正

* translation

* Update Changelog

* status code

* :v:

* モデレーター以上は影響ないことを明記

* :art:

* update changelog

* spdx

* Update update.ts

* refactor

* eliminate `screen name`

* remove untracked file

---------

Co-authored-by: KanariKanaru <93921745+kanarikanaru@users.noreply.github.com>
---
 CHANGELOG.md                                  |  3 +++
 locales/index.d.ts                            | 16 +++++++++++++
 locales/ja-JP.yml                             |  4 ++++
 ...8634286056-prohibitedWordsForNameOfUser.js | 14 +++++++++++
 packages/backend/src/models/Meta.ts           |  5 ++++
 .../src/server/api/endpoints/admin/meta.ts    |  8 +++++++
 .../server/api/endpoints/admin/update-meta.ts |  8 +++++++
 .../src/server/api/endpoints/i/update.ts      | 22 ++++++++++++++++-
 .../components/MkUserSetupDialog.Profile.vue  |  5 ++++
 packages/frontend/src/os.ts                   |  8 +++++--
 .../frontend/src/pages/admin/moderation.vue   | 24 ++++++++++++++++++-
 .../frontend/src/pages/settings/profile.vue   | 11 ++++++++-
 .../frontend/src/scripts/get-note-menu.ts     | 11 ++++-----
 packages/misskey-js/src/autogen/types.ts      |  2 ++
 14 files changed, 129 insertions(+), 12 deletions(-)
 create mode 100644 packages/backend/migration/1728634286056-prohibitedWordsForNameOfUser.js

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 143b63f7b2..130eb00b77 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,9 @@
 7日間活動していない場合は自動的に招待制へと移行(コントロールパネル -> モデレーション -> "誰でも新規登録できるようにする"をオフに変更)するようになりました。  
 詳細な経緯は https://github.com/misskey-dev/misskey/issues/13437 をご確認ください。
 
+### General
+- Feat: ユーザーの名前に禁止ワードを設定できるように
+
 ### Client
 - Enhance: l10nの更新
 - Fix: メールアドレス不要でCaptchaが有効な場合にアカウント登録完了後自動でのログインに失敗する問題を修正
diff --git a/locales/index.d.ts b/locales/index.d.ts
index 2dca73bfa6..7585410291 100644
--- a/locales/index.d.ts
+++ b/locales/index.d.ts
@@ -5170,6 +5170,22 @@ export interface Locale extends ILocale {
      * CAPTCHAのテストを目的とした機能です。<strong>本番環境で使用しないでください。</strong>
      */
     "testCaptchaWarning": string;
+    /**
+     * 禁止ワード(ユーザーの名前)
+     */
+    "prohibitedWordsForNameOfUser": string;
+    /**
+     * このリストに含まれる文字列がユーザーの名前に含まれる場合、ユーザーの名前の変更を拒否します。モデレーター権限を持つユーザーはこの制限の影響を受けません。
+     */
+    "prohibitedWordsForNameOfUserDescription": string;
+    /**
+     * 変更しようとした名前に禁止された文字列が含まれています
+     */
+    "yourNameContainsProhibitedWords": string;
+    /**
+     * 名前に禁止されている文字列が含まれています。この名前を使用したい場合は、サーバー管理者にお問い合わせください。
+     */
+    "yourNameContainsProhibitedWordsDescription": string;
     "_abuseUserReport": {
         /**
          * 転送
diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml
index 440ffa9306..330f7ef473 100644
--- a/locales/ja-JP.yml
+++ b/locales/ja-JP.yml
@@ -1288,6 +1288,10 @@ passkeyVerificationSucceededButPasswordlessLoginDisabled: "パスキーの検証
 messageToFollower: "フォロワーへのメッセージ"
 target: "対象"
 testCaptchaWarning: "CAPTCHAのテストを目的とした機能です。<strong>本番環境で使用しないでください。</strong>"
+prohibitedWordsForNameOfUser: "禁止ワード(ユーザーの名前)"
+prohibitedWordsForNameOfUserDescription: "このリストに含まれる文字列がユーザーの名前に含まれる場合、ユーザーの名前の変更を拒否します。モデレーター権限を持つユーザーはこの制限の影響を受けません。"
+yourNameContainsProhibitedWords: "変更しようとした名前に禁止された文字列が含まれています"
+yourNameContainsProhibitedWordsDescription: "名前に禁止されている文字列が含まれています。この名前を使用したい場合は、サーバー管理者にお問い合わせください。"
 
 _abuseUserReport:
   forward: "転送"
diff --git a/packages/backend/migration/1728634286056-prohibitedWordsForNameOfUser.js b/packages/backend/migration/1728634286056-prohibitedWordsForNameOfUser.js
new file mode 100644
index 0000000000..36e698d120
--- /dev/null
+++ b/packages/backend/migration/1728634286056-prohibitedWordsForNameOfUser.js
@@ -0,0 +1,14 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+export class ProhibitedWordsForNameOfUser1728634286056 {
+		async up(queryRunner) {
+			await queryRunner.query(`ALTER TABLE "meta" ADD "prohibitedWordsForNameOfUser" character varying(1024) array NOT NULL DEFAULT '{}'`);
+		}
+
+		async down(queryRunner) {
+			await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "prohibitedWordsForNameOfUser"`);
+		}
+}
diff --git a/packages/backend/src/models/Meta.ts b/packages/backend/src/models/Meta.ts
index fd007de6c6..5ceee1c3f5 100644
--- a/packages/backend/src/models/Meta.ts
+++ b/packages/backend/src/models/Meta.ts
@@ -81,6 +81,11 @@ export class MiMeta {
 	})
 	public prohibitedWords: string[];
 
+	@Column('varchar', {
+		length: 1024, array: true, default: '{}',
+	})
+	public prohibitedWordsForNameOfUser: string[];
+
 	@Column('varchar', {
 		length: 1024, array: true, default: '{}',
 	})
diff --git a/packages/backend/src/server/api/endpoints/admin/meta.ts b/packages/backend/src/server/api/endpoints/admin/meta.ts
index abb3c17be3..16cbbc9aa4 100644
--- a/packages/backend/src/server/api/endpoints/admin/meta.ts
+++ b/packages/backend/src/server/api/endpoints/admin/meta.ts
@@ -177,6 +177,13 @@ export const meta = {
 					type: 'string',
 				},
 			},
+			prohibitedWordsForNameOfUser: {
+				type: 'array',
+				optional: false, nullable: false,
+				items: {
+					type: 'string',
+				},
+			},
 			bannedEmailDomains: {
 				type: 'array',
 				optional: true, nullable: false,
@@ -586,6 +593,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 				mediaSilencedHosts: instance.mediaSilencedHosts,
 				sensitiveWords: instance.sensitiveWords,
 				prohibitedWords: instance.prohibitedWords,
+				prohibitedWordsForNameOfUser: instance.prohibitedWordsForNameOfUser,
 				preservedUsernames: instance.preservedUsernames,
 				hcaptchaSecretKey: instance.hcaptchaSecretKey,
 				mcaptchaSecretKey: instance.mcaptchaSecretKey,
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 e97ac4e2b9..536645e0d7 100644
--- a/packages/backend/src/server/api/endpoints/admin/update-meta.ts
+++ b/packages/backend/src/server/api/endpoints/admin/update-meta.ts
@@ -46,6 +46,11 @@ export const paramDef = {
 				type: 'string',
 			},
 		},
+		prohibitedWordsForNameOfUser: {
+			type: 'array', nullable: true, items: {
+				type: 'string',
+			},
+		},
 		themeColor: { type: 'string', nullable: true, pattern: '^#[0-9a-fA-F]{6}$' },
 		mascotImageUrl: { type: 'string', nullable: true },
 		bannerUrl: { type: 'string', nullable: true },
@@ -214,6 +219,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 			if (Array.isArray(ps.prohibitedWords)) {
 				set.prohibitedWords = ps.prohibitedWords.filter(Boolean);
 			}
+			if (Array.isArray(ps.prohibitedWordsForNameOfUser)) {
+				set.prohibitedWordsForNameOfUser = ps.prohibitedWordsForNameOfUser.filter(Boolean);
+			}
 			if (Array.isArray(ps.silencedHosts)) {
 				let lastValue = '';
 				set.silencedHosts = ps.silencedHosts.sort().filter((h) => {
diff --git a/packages/backend/src/server/api/endpoints/i/update.ts b/packages/backend/src/server/api/endpoints/i/update.ts
index 798bd98cf1..0b35005a87 100644
--- a/packages/backend/src/server/api/endpoints/i/update.ts
+++ b/packages/backend/src/server/api/endpoints/i/update.ts
@@ -11,7 +11,7 @@ import { JSDOM } from 'jsdom';
 import { extractCustomEmojisFromMfm } from '@/misc/extract-custom-emojis-from-mfm.js';
 import { extractHashtags } from '@/misc/extract-hashtags.js';
 import * as Acct from '@/misc/acct.js';
-import type { UsersRepository, DriveFilesRepository, UserProfilesRepository, PagesRepository } from '@/models/_.js';
+import type { UsersRepository, DriveFilesRepository, MiMeta, UserProfilesRepository, PagesRepository } from '@/models/_.js';
 import type { MiLocalUser, MiUser } from '@/models/User.js';
 import { birthdaySchema, descriptionSchema, followedMessageSchema, locationSchema, nameSchema } from '@/models/User.js';
 import type { MiUserProfile } from '@/models/UserProfile.js';
@@ -22,6 +22,7 @@ import { UserEntityService } from '@/core/entities/UserEntityService.js';
 import { GlobalEventService } from '@/core/GlobalEventService.js';
 import { UserFollowingService } from '@/core/UserFollowingService.js';
 import { AccountUpdateService } from '@/core/AccountUpdateService.js';
+import { UtilityService } from '@/core/UtilityService.js';
 import { HashtagService } from '@/core/HashtagService.js';
 import { DI } from '@/di-symbols.js';
 import { RolePolicies, RoleService } from '@/core/RoleService.js';
@@ -114,6 +115,13 @@ export const meta = {
 			code: 'RESTRICTED_BY_ROLE',
 			id: '8feff0ba-5ab5-585b-31f4-4df816663fad',
 		},
+
+		nameContainsProhibitedWords: {
+			message: 'Your new name contains prohibited words.',
+			code: 'YOUR_NAME_CONTAINS_PROHIBITED_WORDS',
+			id: '0b3f9f6a-2f4d-4b1f-9fb4-49d3a2fd7191',
+			httpStatusCode: 422,
+		},
 	},
 
 	res: {
@@ -223,6 +231,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 		@Inject(DI.config)
 		private config: Config,
 
+		@Inject(DI.meta)
+		private instanceMeta: MiMeta,
+
 		@Inject(DI.usersRepository)
 		private usersRepository: UsersRepository,
 
@@ -247,6 +258,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 		private cacheService: CacheService,
 		private httpRequestService: HttpRequestService,
 		private avatarDecorationService: AvatarDecorationService,
+		private utilityService: UtilityService,
 	) {
 		super(meta, paramDef, async (ps, _user, token) => {
 			const user = await this.usersRepository.findOneByOrFail({ id: _user.id }) as MiLocalUser;
@@ -449,6 +461,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 			const newFields = profileUpdates.fields === undefined ? profile.fields : profileUpdates.fields;
 
 			if (newName != null) {
+				let hasProhibitedWords = false;
+				if (!await this.roleService.isModerator(user)) {
+					hasProhibitedWords = this.utilityService.isKeyWordIncluded(newName, this.instanceMeta.prohibitedWordsForNameOfUser);
+				}
+				if (hasProhibitedWords) {
+					throw new ApiError(meta.errors.nameContainsProhibitedWords);
+				}
+
 				const tokens = mfm.parseSimple(newName);
 				emojis = emojis.concat(extractCustomEmojisFromMfm(tokens));
 			}
diff --git a/packages/frontend/src/components/MkUserSetupDialog.Profile.vue b/packages/frontend/src/components/MkUserSetupDialog.Profile.vue
index 3194641cdb..7cb48f6afb 100644
--- a/packages/frontend/src/components/MkUserSetupDialog.Profile.vue
+++ b/packages/frontend/src/components/MkUserSetupDialog.Profile.vue
@@ -51,6 +51,11 @@ watch(name, () => {
 		// 空文字列をnullにしたいので??は使うな
 		// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
 		name: name.value || null,
+	}, undefined, {
+		'0b3f9f6a-2f4d-4b1f-9fb4-49d3a2fd7191': {
+			title: i18n.ts.yourNameContainsProhibitedWords,
+			text: i18n.ts.yourNameContainsProhibitedWordsDescription,
+		},
 	});
 });
 
diff --git a/packages/frontend/src/os.ts b/packages/frontend/src/os.ts
index 60e4218a48..4d41cf5bc0 100644
--- a/packages/frontend/src/os.ts
+++ b/packages/frontend/src/os.ts
@@ -10,6 +10,7 @@ import { EventEmitter } from 'eventemitter3';
 import * as Misskey from 'misskey-js';
 import type { ComponentProps as CP } from 'vue-component-type-helpers';
 import type { Form, GetFormResultType } from '@/scripts/form.js';
+import type { MenuItem } from '@/types/menu.js';
 import { misskeyApi } from '@/scripts/misskey-api.js';
 import { defaultStore } from '@/store.js';
 import { i18n } from '@/i18n.js';
@@ -22,7 +23,6 @@ import MkPasswordDialog from '@/components/MkPasswordDialog.vue';
 import MkEmojiPickerDialog from '@/components/MkEmojiPickerDialog.vue';
 import MkPopupMenu from '@/components/MkPopupMenu.vue';
 import MkContextMenu from '@/components/MkContextMenu.vue';
-import type { MenuItem } from '@/types/menu.js';
 import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
 import { pleaseLogin } from '@/scripts/please-login.js';
 import { showMovedDialog } from '@/scripts/show-moved-dialog.js';
@@ -35,6 +35,7 @@ export const apiWithDialog = (<E extends keyof Misskey.Endpoints = keyof Misskey
 	endpoint: E,
 	data: P = {} as any,
 	token?: string | null | undefined,
+	customErrors?: Record<string, { title?: string; text: string; }>,
 ) => {
 	const promise = misskeyApi(endpoint, data, token);
 	promiseDialog(promise, null, async (err) => {
@@ -77,6 +78,9 @@ export const apiWithDialog = (<E extends keyof Misskey.Endpoints = keyof Misskey
 		} else if (err.message.startsWith('Unexpected token')) {
 			title = i18n.ts.gotInvalidResponseError;
 			text = i18n.ts.gotInvalidResponseErrorDescription;
+		} else if (customErrors && customErrors[err.id] != null) {
+			title = customErrors[err.id].title;
+			text = customErrors[err.id].text;
 		}
 		alert({
 			type: 'error',
@@ -86,7 +90,7 @@ export const apiWithDialog = (<E extends keyof Misskey.Endpoints = keyof Misskey
 	});
 
 	return promise;
-}) as typeof misskeyApi;
+});
 
 export function promiseDialog<T extends Promise<any>>(
 	promise: T,
diff --git a/packages/frontend/src/pages/admin/moderation.vue b/packages/frontend/src/pages/admin/moderation.vue
index 04d23b1358..5d8a581b2e 100644
--- a/packages/frontend/src/pages/admin/moderation.vue
+++ b/packages/frontend/src/pages/admin/moderation.vue
@@ -57,6 +57,18 @@ SPDX-License-Identifier: AGPL-3.0-only
 						</div>
 					</MkFolder>
 
+					<MkFolder>
+						<template #icon><i class="ti ti-user-x"></i></template>
+						<template #label>{{ i18n.ts.prohibitedWordsForNameOfUser }}</template>
+
+						<div class="_gaps">
+							<MkTextarea v-model="prohibitedWordsForNameOfUser">
+								<template #caption>{{ i18n.ts.prohibitedWordsForNameOfUserDescription }}<br>{{ i18n.ts.prohibitedWordsDescription2 }}</template>
+							</MkTextarea>
+							<MkButton primary @click="save_prohibitedWordsForNameOfUser">{{ i18n.ts.save }}</MkButton>
+						</div>
+					</MkFolder>
+
 					<MkFolder>
 						<template #icon><i class="ti ti-eye-off"></i></template>
 						<template #label>{{ i18n.ts.hiddenTags }}</template>
@@ -131,6 +143,7 @@ const enableRegistration = ref<boolean>(false);
 const emailRequiredForSignup = ref<boolean>(false);
 const sensitiveWords = ref<string>('');
 const prohibitedWords = ref<string>('');
+const prohibitedWordsForNameOfUser = ref<string>('');
 const hiddenTags = ref<string>('');
 const preservedUsernames = ref<string>('');
 const blockedHosts = ref<string>('');
@@ -143,10 +156,11 @@ async function init() {
 	emailRequiredForSignup.value = meta.emailRequiredForSignup;
 	sensitiveWords.value = meta.sensitiveWords.join('\n');
 	prohibitedWords.value = meta.prohibitedWords.join('\n');
+	prohibitedWordsForNameOfUser.value = meta.prohibitedWordsForNameOfUser.join('\n');
 	hiddenTags.value = meta.hiddenTags.join('\n');
 	preservedUsernames.value = meta.preservedUsernames.join('\n');
 	blockedHosts.value = meta.blockedHosts.join('\n');
-	silencedHosts.value = meta.silencedHosts.join('\n');
+	silencedHosts.value = meta.silencedHosts?.join('\n') ?? '';
 	mediaSilencedHosts.value = meta.mediaSilencedHosts.join('\n');
 }
 
@@ -190,6 +204,14 @@ function save_prohibitedWords() {
 	});
 }
 
+function save_prohibitedWordsForNameOfUser() {
+	os.apiWithDialog('admin/update-meta', {
+		prohibitedWordsForNameOfUser: prohibitedWordsForNameOfUser.value.split('\n'),
+	}).then(() => {
+		fetchInstance(true);
+	});
+}
+
 function save_hiddenTags() {
 	os.apiWithDialog('admin/update-meta', {
 		hiddenTags: hiddenTags.value.split('\n'),
diff --git a/packages/frontend/src/pages/settings/profile.vue b/packages/frontend/src/pages/settings/profile.vue
index 0d61f8d851..561894d2b7 100644
--- a/packages/frontend/src/pages/settings/profile.vue
+++ b/packages/frontend/src/pages/settings/profile.vue
@@ -142,13 +142,17 @@ const Sortable = defineAsyncComponent(() => import('vuedraggable').then(x => x.d
 
 const reactionAcceptance = computed(defaultStore.makeGetterSetter('reactionAcceptance'));
 
+function assertVaildLang(lang: string | null): lang is keyof typeof langmap {
+	return lang != null && lang in langmap;
+}
+
 const profile = reactive({
 	name: $i.name,
 	description: $i.description,
 	followedMessage: $i.followedMessage,
 	location: $i.location,
 	birthday: $i.birthday,
-	lang: $i.lang,
+	lang: assertVaildLang($i.lang) ? $i.lang : null,
 	isBot: $i.isBot ?? false,
 	isCat: $i.isCat ?? false,
 });
@@ -202,6 +206,11 @@ function save() {
 		lang: profile.lang || null,
 		isBot: !!profile.isBot,
 		isCat: !!profile.isCat,
+	}, undefined, {
+		'0b3f9f6a-2f4d-4b1f-9fb4-49d3a2fd7191': {
+			title: i18n.ts.yourNameContainsProhibitedWords,
+			text: i18n.ts.yourNameContainsProhibitedWordsDescription,
+		},
 	});
 	globalEvents.emit('requestClearPageCache');
 	claimAchievement('profileFilled');
diff --git a/packages/frontend/src/scripts/get-note-menu.ts b/packages/frontend/src/scripts/get-note-menu.ts
index 4ffa0ab94d..c1846b0589 100644
--- a/packages/frontend/src/scripts/get-note-menu.ts
+++ b/packages/frontend/src/scripts/get-note-menu.ts
@@ -245,13 +245,10 @@ export function getNoteMenu(props: {
 	function togglePin(pin: boolean): void {
 		os.apiWithDialog(pin ? 'i/pin' : 'i/unpin', {
 			noteId: appearNote.id,
-		}, undefined, null, res => {
-			if (res.id === '72dab508-c64d-498f-8740-a8eec1ba385a') {
-				os.alert({
-					type: 'error',
-					text: i18n.ts.pinLimitExceeded,
-				});
-			}
+		}, undefined, {
+			'72dab508-c64d-498f-8740-a8eec1ba385a': {
+				text: i18n.ts.pinLimitExceeded,
+			},
 		});
 	}
 
diff --git a/packages/misskey-js/src/autogen/types.ts b/packages/misskey-js/src/autogen/types.ts
index e40cb050fd..f61d72e280 100644
--- a/packages/misskey-js/src/autogen/types.ts
+++ b/packages/misskey-js/src/autogen/types.ts
@@ -5124,6 +5124,7 @@ export type operations = {
             blockedHosts: string[];
             sensitiveWords: string[];
             prohibitedWords: string[];
+            prohibitedWordsForNameOfUser: string[];
             bannedEmailDomains?: string[];
             preservedUsernames: string[];
             hcaptchaSecretKey: string | null;
@@ -9461,6 +9462,7 @@ export type operations = {
           blockedHosts?: string[] | null;
           sensitiveWords?: string[] | null;
           prohibitedWords?: string[] | null;
+          prohibitedWordsForNameOfUser?: string[] | null;
           themeColor?: string | null;
           mascotImageUrl?: string | null;
           bannerUrl?: string | null;

From ff47fef5725ba31efc7016534c2d9db8b0ad242a Mon Sep 17 00:00:00 2001
From: syuilo <4439005+syuilo@users.noreply.github.com>
Date: Sun, 13 Oct 2024 20:22:16 +0900
Subject: [PATCH 095/121] =?UTF-8?q?feat:=20=E3=83=AA=E3=83=A2=E3=83=BC?=
 =?UTF-8?q?=E3=83=88=E3=82=B5=E3=83=BC=E3=83=90=E3=83=BC=E3=81=AE=E3=82=B5?=
 =?UTF-8?q?=E3=83=BC=E3=83=90=E3=83=BC=E6=83=85=E5=A0=B1=E3=82=92=E5=8F=8E?=
 =?UTF-8?q?=E9=9B=86=E3=81=97=E3=81=AA=E3=81=84=E3=82=AA=E3=83=97=E3=82=B7?=
 =?UTF-8?q?=E3=83=A7=E3=83=B3=20(#14634)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

* wip

* wip

* Update FetchInstanceMetadataService.ts

* Update FetchInstanceMetadataService.ts

* Update types.ts
---
 locales/index.d.ts                            |  4 ++
 locales/ja-JP.yml                             |  1 +
 ...020265-enableStatsForFederatedInstances.js | 16 +++++
 .../backend/src/core/AccountMoveService.ts    | 16 ++---
 .../src/core/FederatedInstanceService.ts      | 20 ++++++-
 .../src/core/FetchInstanceMetadataService.ts  |  2 +-
 .../backend/src/core/NoteCreateService.ts     | 16 ++---
 .../backend/src/core/NoteDeleteService.ts     | 16 ++---
 .../backend/src/core/UserFollowingService.ts  | 60 ++++++++++---------
 .../activitypub/models/ApPersonService.ts     | 16 ++---
 packages/backend/src/models/Meta.ts           |  5 ++
 .../processors/DeliverProcessorService.ts     | 31 ++++++----
 .../queue/processors/InboxProcessorService.ts | 20 ++++---
 .../src/server/api/endpoints/admin/meta.ts    |  5 ++
 .../server/api/endpoints/admin/update-meta.ts |  5 ++
 .../test/unit/FetchInstanceMetadataService.ts | 20 +++----
 .../frontend/src/pages/admin/performance.vue  | 16 +++++
 packages/misskey-js/src/autogen/types.ts      |  2 +
 18 files changed, 185 insertions(+), 86 deletions(-)
 create mode 100644 packages/backend/migration/1727318020265-enableStatsForFederatedInstances.js

diff --git a/locales/index.d.ts b/locales/index.d.ts
index 7585410291..3ffb67f31c 100644
--- a/locales/index.d.ts
+++ b/locales/index.d.ts
@@ -4366,6 +4366,10 @@ export interface Locale extends ILocale {
      * リモートサーバーのチャートを生成
      */
     "enableChartsForFederatedInstances": string;
+    /**
+     * リモートサーバーの情報を取得
+     */
+    "enableStatsForFederatedInstances": string;
     /**
      * ノートのアクションにクリップを追加
      */
diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml
index 330f7ef473..919be1e3ec 100644
--- a/locales/ja-JP.yml
+++ b/locales/ja-JP.yml
@@ -1087,6 +1087,7 @@ retryAllQueuesConfirmTitle: "今すぐ再試行しますか?"
 retryAllQueuesConfirmText: "一時的にサーバーの負荷が増大することがあります。"
 enableChartsForRemoteUser: "リモートユーザーのチャートを生成"
 enableChartsForFederatedInstances: "リモートサーバーのチャートを生成"
+enableStatsForFederatedInstances: "リモートサーバーの情報を取得"
 showClipButtonInNoteFooter: "ノートのアクションにクリップを追加"
 reactionsDisplaySize: "リアクションの表示サイズ"
 limitWidthOfReaction: "リアクションの最大横幅を制限し、縮小して表示する"
diff --git a/packages/backend/migration/1727318020265-enableStatsForFederatedInstances.js b/packages/backend/migration/1727318020265-enableStatsForFederatedInstances.js
new file mode 100644
index 0000000000..4ff520172b
--- /dev/null
+++ b/packages/backend/migration/1727318020265-enableStatsForFederatedInstances.js
@@ -0,0 +1,16 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+export class EnableStatsForFederatedInstances1727318020265 {
+    name = 'EnableStatsForFederatedInstances1727318020265'
+
+    async up(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "meta" ADD "enableStatsForFederatedInstances" boolean NOT NULL DEFAULT true`);
+    }
+
+    async down(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "enableStatsForFederatedInstances"`);
+    }
+}
diff --git a/packages/backend/src/core/AccountMoveService.ts b/packages/backend/src/core/AccountMoveService.ts
index 6e3125044c..24d11f29ff 100644
--- a/packages/backend/src/core/AccountMoveService.ts
+++ b/packages/backend/src/core/AccountMoveService.ts
@@ -274,13 +274,15 @@ export class AccountMoveService {
 		}
 
 		// Update instance stats by decreasing remote followers count by the number of local followers who were following the old account.
-		if (this.userEntityService.isRemoteUser(oldAccount)) {
-			this.federatedInstanceService.fetch(oldAccount.host).then(async i => {
-				this.instancesRepository.decrement({ id: i.id }, 'followersCount', localFollowerIds.length);
-				if (this.meta.enableChartsForFederatedInstances) {
-					this.instanceChart.updateFollowers(i.host, false);
-				}
-			});
+		if (this.meta.enableStatsForFederatedInstances) {
+			if (this.userEntityService.isRemoteUser(oldAccount)) {
+				this.federatedInstanceService.fetchOrRegister(oldAccount.host).then(async i => {
+					this.instancesRepository.decrement({ id: i.id }, 'followersCount', localFollowerIds.length);
+					if (this.meta.enableChartsForFederatedInstances) {
+						this.instanceChart.updateFollowers(i.host, false);
+					}
+				});
+			}
 		}
 
 		// FIXME: expensive?
diff --git a/packages/backend/src/core/FederatedInstanceService.ts b/packages/backend/src/core/FederatedInstanceService.ts
index 7aeeb78178..73bbf03b26 100644
--- a/packages/backend/src/core/FederatedInstanceService.ts
+++ b/packages/backend/src/core/FederatedInstanceService.ts
@@ -47,7 +47,7 @@ export class FederatedInstanceService implements OnApplicationShutdown {
 	}
 
 	@bindThis
-	public async fetch(host: string): Promise<MiInstance> {
+	public async fetchOrRegister(host: string): Promise<MiInstance> {
 		host = this.utilityService.toPuny(host);
 
 		const cached = await this.federatedInstanceCache.get(host);
@@ -70,6 +70,24 @@ export class FederatedInstanceService implements OnApplicationShutdown {
 		}
 	}
 
+	@bindThis
+	public async fetch(host: string): Promise<MiInstance | null> {
+		host = this.utilityService.toPuny(host);
+
+		const cached = await this.federatedInstanceCache.get(host);
+		if (cached !== undefined) return cached;
+
+		const index = await this.instancesRepository.findOneBy({ host });
+
+		if (index == null) {
+			this.federatedInstanceCache.set(host, null);
+			return null;
+		} else {
+			this.federatedInstanceCache.set(host, index);
+			return index;
+		}
+	}
+
 	@bindThis
 	public async update(id: MiInstance['id'], data: Partial<MiInstance>): Promise<void> {
 		const result = await this.instancesRepository.createQueryBuilder().update()
diff --git a/packages/backend/src/core/FetchInstanceMetadataService.ts b/packages/backend/src/core/FetchInstanceMetadataService.ts
index aa16468ecb..987999bce7 100644
--- a/packages/backend/src/core/FetchInstanceMetadataService.ts
+++ b/packages/backend/src/core/FetchInstanceMetadataService.ts
@@ -82,7 +82,7 @@ export class FetchInstanceMetadataService {
 
 		try {
 			if (!force) {
-				const _instance = await this.federatedInstanceService.fetch(host);
+				const _instance = await this.federatedInstanceService.fetchOrRegister(host);
 				const now = Date.now();
 				if (_instance && _instance.infoUpdatedAt && (now - _instance.infoUpdatedAt.getTime() < 1000 * 60 * 60 * 24)) {
 					// unlock at the finally caluse
diff --git a/packages/backend/src/core/NoteCreateService.ts b/packages/backend/src/core/NoteCreateService.ts
index 0ce57f16e6..3647fa7231 100644
--- a/packages/backend/src/core/NoteCreateService.ts
+++ b/packages/backend/src/core/NoteCreateService.ts
@@ -511,13 +511,15 @@ export class NoteCreateService implements OnApplicationShutdown {
 		}
 
 		// Register host
-		if (this.userEntityService.isRemoteUser(user)) {
-			this.federatedInstanceService.fetch(user.host).then(async i => {
-				this.updateNotesCountQueue.enqueue(i.id, 1);
-				if (this.meta.enableChartsForFederatedInstances) {
-					this.instanceChart.updateNote(i.host, note, true);
-				}
-			});
+		if (this.meta.enableStatsForFederatedInstances) {
+			if (this.userEntityService.isRemoteUser(user)) {
+				this.federatedInstanceService.fetchOrRegister(user.host).then(async i => {
+					this.updateNotesCountQueue.enqueue(i.id, 1);
+					if (this.meta.enableChartsForFederatedInstances) {
+						this.instanceChart.updateNote(i.host, note, true);
+					}
+				});
+			}
 		}
 
 		// ハッシュタグ更新
diff --git a/packages/backend/src/core/NoteDeleteService.ts b/packages/backend/src/core/NoteDeleteService.ts
index f9f8ace386..4ecd2592b2 100644
--- a/packages/backend/src/core/NoteDeleteService.ts
+++ b/packages/backend/src/core/NoteDeleteService.ts
@@ -106,13 +106,15 @@ export class NoteDeleteService {
 				this.perUserNotesChart.update(user, note, false);
 			}
 
-			if (this.userEntityService.isRemoteUser(user)) {
-				this.federatedInstanceService.fetch(user.host).then(async i => {
-					this.instancesRepository.decrement({ id: i.id }, 'notesCount', 1);
-					if (this.meta.enableChartsForFederatedInstances) {
-						this.instanceChart.updateNote(i.host, note, false);
-					}
-				});
+			if (this.meta.enableStatsForFederatedInstances) {
+				if (this.userEntityService.isRemoteUser(user)) {
+					this.federatedInstanceService.fetchOrRegister(user.host).then(async i => {
+						this.instancesRepository.decrement({ id: i.id }, 'notesCount', 1);
+						if (this.meta.enableChartsForFederatedInstances) {
+							this.instanceChart.updateNote(i.host, note, false);
+						}
+					});
+				}
 			}
 		}
 
diff --git a/packages/backend/src/core/UserFollowingService.ts b/packages/backend/src/core/UserFollowingService.ts
index 77e7b60bea..8963003057 100644
--- a/packages/backend/src/core/UserFollowingService.ts
+++ b/packages/backend/src/core/UserFollowingService.ts
@@ -305,20 +305,22 @@ export class UserFollowingService implements OnModuleInit {
 			//#endregion
 
 			//#region Update instance stats
-			if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee)) {
-				this.federatedInstanceService.fetch(follower.host).then(async i => {
-					this.instancesRepository.increment({ id: i.id }, 'followingCount', 1);
-					if (this.meta.enableChartsForFederatedInstances) {
-						this.instanceChart.updateFollowing(i.host, true);
-					}
-				});
-			} else if (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee)) {
-				this.federatedInstanceService.fetch(followee.host).then(async i => {
-					this.instancesRepository.increment({ id: i.id }, 'followersCount', 1);
-					if (this.meta.enableChartsForFederatedInstances) {
-						this.instanceChart.updateFollowers(i.host, true);
-					}
-				});
+			if (this.meta.enableStatsForFederatedInstances) {
+				if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee)) {
+					this.federatedInstanceService.fetchOrRegister(follower.host).then(async i => {
+						this.instancesRepository.increment({ id: i.id }, 'followingCount', 1);
+						if (this.meta.enableChartsForFederatedInstances) {
+							this.instanceChart.updateFollowing(i.host, true);
+						}
+					});
+				} else if (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee)) {
+					this.federatedInstanceService.fetchOrRegister(followee.host).then(async i => {
+						this.instancesRepository.increment({ id: i.id }, 'followersCount', 1);
+						if (this.meta.enableChartsForFederatedInstances) {
+							this.instanceChart.updateFollowers(i.host, true);
+						}
+					});
+				}
 			}
 			//#endregion
 
@@ -437,20 +439,22 @@ export class UserFollowingService implements OnModuleInit {
 			//#endregion
 
 			//#region Update instance stats
-			if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee)) {
-				this.federatedInstanceService.fetch(follower.host).then(async i => {
-					this.instancesRepository.decrement({ id: i.id }, 'followingCount', 1);
-					if (this.meta.enableChartsForFederatedInstances) {
-						this.instanceChart.updateFollowing(i.host, false);
-					}
-				});
-			} else if (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee)) {
-				this.federatedInstanceService.fetch(followee.host).then(async i => {
-					this.instancesRepository.decrement({ id: i.id }, 'followersCount', 1);
-					if (this.meta.enableChartsForFederatedInstances) {
-						this.instanceChart.updateFollowers(i.host, false);
-					}
-				});
+			if (this.meta.enableStatsForFederatedInstances) {
+				if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee)) {
+					this.federatedInstanceService.fetchOrRegister(follower.host).then(async i => {
+						this.instancesRepository.decrement({ id: i.id }, 'followingCount', 1);
+						if (this.meta.enableChartsForFederatedInstances) {
+							this.instanceChart.updateFollowing(i.host, false);
+						}
+					});
+				} else if (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee)) {
+					this.federatedInstanceService.fetchOrRegister(followee.host).then(async i => {
+						this.instancesRepository.decrement({ id: i.id }, 'followersCount', 1);
+						if (this.meta.enableChartsForFederatedInstances) {
+							this.instanceChart.updateFollowers(i.host, false);
+						}
+					});
+				}
 			}
 			//#endregion
 
diff --git a/packages/backend/src/core/activitypub/models/ApPersonService.ts b/packages/backend/src/core/activitypub/models/ApPersonService.ts
index e042a85782..73281078e5 100644
--- a/packages/backend/src/core/activitypub/models/ApPersonService.ts
+++ b/packages/backend/src/core/activitypub/models/ApPersonService.ts
@@ -408,13 +408,15 @@ export class ApPersonService implements OnModuleInit {
 		this.cacheService.uriPersonCache.set(user.uri, user);
 
 		// Register host
-		this.federatedInstanceService.fetch(host).then(i => {
-			this.instancesRepository.increment({ id: i.id }, 'usersCount', 1);
-			this.fetchInstanceMetadataService.fetchInstanceMetadata(i);
-			if (this.meta.enableChartsForFederatedInstances) {
-				this.instanceChart.newUser(i.host);
-			}
-		});
+		if (this.meta.enableStatsForFederatedInstances) {
+			this.federatedInstanceService.fetchOrRegister(host).then(i => {
+				this.instancesRepository.increment({ id: i.id }, 'usersCount', 1);
+				if (this.meta.enableChartsForFederatedInstances) {
+					this.instanceChart.newUser(i.host);
+				}
+				this.fetchInstanceMetadataService.fetchInstanceMetadata(i);
+			});
+		}
 
 		this.usersChart.update(user, true);
 
diff --git a/packages/backend/src/models/Meta.ts b/packages/backend/src/models/Meta.ts
index 5ceee1c3f5..ad5e31ad6f 100644
--- a/packages/backend/src/models/Meta.ts
+++ b/packages/backend/src/models/Meta.ts
@@ -529,6 +529,11 @@ export class MiMeta {
 	})
 	public enableChartsForFederatedInstances: boolean;
 
+	@Column('boolean', {
+		default: true,
+	})
+	public enableStatsForFederatedInstances: boolean;
+
 	@Column('boolean', {
 		default: false,
 	})
diff --git a/packages/backend/src/queue/processors/DeliverProcessorService.ts b/packages/backend/src/queue/processors/DeliverProcessorService.ts
index 9590a4fe71..5a16496011 100644
--- a/packages/backend/src/queue/processors/DeliverProcessorService.ts
+++ b/packages/backend/src/queue/processors/DeliverProcessorService.ts
@@ -74,8 +74,17 @@ export class DeliverProcessorService {
 		try {
 			await this.apRequestService.signedPost(job.data.user, job.data.to, job.data.content, job.data.digest);
 
-			// Update stats
-			this.federatedInstanceService.fetch(host).then(i => {
+			this.apRequestChart.deliverSucc();
+			this.federationChart.deliverd(host, true);
+
+			// Update instance stats
+			process.nextTick(async () => {
+				const i = await (this.meta.enableStatsForFederatedInstances
+					? this.federatedInstanceService.fetchOrRegister(host)
+					: this.federatedInstanceService.fetch(host));
+
+				if (i == null) return;
+
 				if (i.isNotResponding) {
 					this.federatedInstanceService.update(i.id, {
 						isNotResponding: false,
@@ -83,9 +92,9 @@ export class DeliverProcessorService {
 					});
 				}
 
-				this.fetchInstanceMetadataService.fetchInstanceMetadata(i);
-				this.apRequestChart.deliverSucc();
-				this.federationChart.deliverd(i.host, true);
+				if (this.meta.enableStatsForFederatedInstances) {
+					this.fetchInstanceMetadataService.fetchInstanceMetadata(i);
+				}
 
 				if (this.meta.enableChartsForFederatedInstances) {
 					this.instanceChart.requestSent(i.host, true);
@@ -94,8 +103,11 @@ export class DeliverProcessorService {
 
 			return 'Success';
 		} catch (res) {
-			// Update stats
-			this.federatedInstanceService.fetch(host).then(i => {
+			this.apRequestChart.deliverFail();
+			this.federationChart.deliverd(host, false);
+
+			// Update instance stats
+			this.federatedInstanceService.fetchOrRegister(host).then(i => {
 				if (!i.isNotResponding) {
 					this.federatedInstanceService.update(i.id, {
 						isNotResponding: true,
@@ -116,9 +128,6 @@ export class DeliverProcessorService {
 					});
 				}
 
-				this.apRequestChart.deliverFail();
-				this.federationChart.deliverd(i.host, false);
-
 				if (this.meta.enableChartsForFederatedInstances) {
 					this.instanceChart.requestSent(i.host, false);
 				}
@@ -129,7 +138,7 @@ export class DeliverProcessorService {
 				if (!res.isRetryable) {
 					// 相手が閉鎖していることを明示しているため、配送停止する
 					if (job.data.isSharedInbox && res.statusCode === 410) {
-						this.federatedInstanceService.fetch(host).then(i => {
+						this.federatedInstanceService.fetchOrRegister(host).then(i => {
 							this.federatedInstanceService.update(i.id, {
 								suspensionState: 'goneSuspended',
 							});
diff --git a/packages/backend/src/queue/processors/InboxProcessorService.ts b/packages/backend/src/queue/processors/InboxProcessorService.ts
index a77c968395..95d764e4d8 100644
--- a/packages/backend/src/queue/processors/InboxProcessorService.ts
+++ b/packages/backend/src/queue/processors/InboxProcessorService.ts
@@ -192,21 +192,27 @@ export class InboxProcessorService implements OnApplicationShutdown {
 			}
 		}
 
-		// Update stats
-		this.federatedInstanceService.fetch(authUser.user.host).then(i => {
+		this.apRequestChart.inbox();
+		this.federationChart.inbox(authUser.user.host);
+
+		// Update instance stats
+		process.nextTick(async () => {
+			const i = await (this.meta.enableStatsForFederatedInstances
+				? this.federatedInstanceService.fetchOrRegister(authUser.user.host)
+				: this.federatedInstanceService.fetch(authUser.user.host));
+
+			if (i == null) return;
+
 			this.updateInstanceQueue.enqueue(i.id, {
 				latestRequestReceivedAt: new Date(),
 				shouldUnsuspend: i.suspensionState === 'autoSuspendedForNotResponding',
 			});
 
-			this.fetchInstanceMetadataService.fetchInstanceMetadata(i);
-
-			this.apRequestChart.inbox();
-			this.federationChart.inbox(i.host);
-
 			if (this.meta.enableChartsForFederatedInstances) {
 				this.instanceChart.requestReceived(i.host);
 			}
+
+			this.fetchInstanceMetadataService.fetchInstanceMetadata(i);
 		});
 
 		// アクティビティを処理
diff --git a/packages/backend/src/server/api/endpoints/admin/meta.ts b/packages/backend/src/server/api/endpoints/admin/meta.ts
index 16cbbc9aa4..64e3cc33bd 100644
--- a/packages/backend/src/server/api/endpoints/admin/meta.ts
+++ b/packages/backend/src/server/api/endpoints/admin/meta.ts
@@ -348,6 +348,10 @@ export const meta = {
 				type: 'boolean',
 				optional: false, nullable: false,
 			},
+			enableStatsForFederatedInstances: {
+				type: 'boolean',
+				optional: false, nullable: false,
+			},
 			enableServerMachineStats: {
 				type: 'boolean',
 				optional: false, nullable: false,
@@ -635,6 +639,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 				truemailAuthKey: instance.truemailAuthKey,
 				enableChartsForRemoteUser: instance.enableChartsForRemoteUser,
 				enableChartsForFederatedInstances: instance.enableChartsForFederatedInstances,
+				enableStatsForFederatedInstances: instance.enableStatsForFederatedInstances,
 				enableServerMachineStats: instance.enableServerMachineStats,
 				enableIdenticonGeneration: instance.enableIdenticonGeneration,
 				bannedEmailDomains: instance.bannedEmailDomains,
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 536645e0d7..38ef0d1de8 100644
--- a/packages/backend/src/server/api/endpoints/admin/update-meta.ts
+++ b/packages/backend/src/server/api/endpoints/admin/update-meta.ts
@@ -136,6 +136,7 @@ export const paramDef = {
 		truemailAuthKey: { type: 'string', nullable: true },
 		enableChartsForRemoteUser: { type: 'boolean' },
 		enableChartsForFederatedInstances: { type: 'boolean' },
+		enableStatsForFederatedInstances: { type: 'boolean' },
 		enableServerMachineStats: { type: 'boolean' },
 		enableIdenticonGeneration: { type: 'boolean' },
 		serverRules: { type: 'array', items: { type: 'string' } },
@@ -578,6 +579,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 				set.enableChartsForFederatedInstances = ps.enableChartsForFederatedInstances;
 			}
 
+			if (ps.enableStatsForFederatedInstances !== undefined) {
+				set.enableStatsForFederatedInstances = ps.enableStatsForFederatedInstances;
+			}
+
 			if (ps.enableServerMachineStats !== undefined) {
 				set.enableServerMachineStats = ps.enableServerMachineStats;
 			}
diff --git a/packages/backend/test/unit/FetchInstanceMetadataService.ts b/packages/backend/test/unit/FetchInstanceMetadataService.ts
index bf8f3ab0e3..1e3605aafc 100644
--- a/packages/backend/test/unit/FetchInstanceMetadataService.ts
+++ b/packages/backend/test/unit/FetchInstanceMetadataService.ts
@@ -8,6 +8,7 @@ process.env.NODE_ENV = 'test';
 import { jest } from '@jest/globals';
 import { Test } from '@nestjs/testing';
 import { Redis } from 'ioredis';
+import type { TestingModule } from '@nestjs/testing';
 import { GlobalModule } from '@/GlobalModule.js';
 import { FetchInstanceMetadataService } from '@/core/FetchInstanceMetadataService.js';
 import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
@@ -16,7 +17,6 @@ import { LoggerService } from '@/core/LoggerService.js';
 import { UtilityService } from '@/core/UtilityService.js';
 import { IdService } from '@/core/IdService.js';
 import { DI } from '@/di-symbols.js';
-import type { TestingModule } from '@nestjs/testing';
 
 function mockRedis() {
 	const hash = {} as any;
@@ -52,7 +52,7 @@ describe('FetchInstanceMetadataService', () => {
 				if (token === HttpRequestService) {
 					return { getJson: jest.fn(), getHtml: jest.fn(), send: jest.fn() };
 				} else if (token === FederatedInstanceService) {
-					return { fetch: jest.fn() };
+					return { fetchOrRegister: jest.fn() };
 				} else if (token === DI.redis) {
 					return mockRedis;
 				}
@@ -75,7 +75,7 @@ describe('FetchInstanceMetadataService', () => {
 	test('Lock and update', async () => {
 		redisClient.set = mockRedis();
 		const now = Date.now();
-		federatedInstanceService.fetch.mockResolvedValue({ infoUpdatedAt: { getTime: () => { return now - 10 * 1000 * 60 * 60 * 24; } } } as any);
+		federatedInstanceService.fetchOrRegister.mockResolvedValue({ infoUpdatedAt: { getTime: () => { return now - 10 * 1000 * 60 * 60 * 24; } } } as any);
 		httpRequestService.getJson.mockImplementation(() => { throw Error(); });
 		const tryLockSpy = jest.spyOn(fetchInstanceMetadataService, 'tryLock');
 		const unlockSpy = jest.spyOn(fetchInstanceMetadataService, 'unlock');
@@ -83,14 +83,14 @@ describe('FetchInstanceMetadataService', () => {
 		await fetchInstanceMetadataService.fetchInstanceMetadata({ host: 'example.com' } as any);
 		expect(tryLockSpy).toHaveBeenCalledTimes(1);
 		expect(unlockSpy).toHaveBeenCalledTimes(1);
-		expect(federatedInstanceService.fetch).toHaveBeenCalledTimes(1);
+		expect(federatedInstanceService.fetchOrRegister).toHaveBeenCalledTimes(1);
 		expect(httpRequestService.getJson).toHaveBeenCalled();
 	});
 
 	test('Lock and don\'t update', async () => {
 		redisClient.set = mockRedis();
 		const now = Date.now();
-		federatedInstanceService.fetch.mockResolvedValue({ infoUpdatedAt: { getTime: () => now } } as any);
+		federatedInstanceService.fetchOrRegister.mockResolvedValue({ infoUpdatedAt: { getTime: () => now } } as any);
 		httpRequestService.getJson.mockImplementation(() => { throw Error(); });
 		const tryLockSpy = jest.spyOn(fetchInstanceMetadataService, 'tryLock');
 		const unlockSpy = jest.spyOn(fetchInstanceMetadataService, 'unlock');
@@ -98,14 +98,14 @@ describe('FetchInstanceMetadataService', () => {
 		await fetchInstanceMetadataService.fetchInstanceMetadata({ host: 'example.com' } as any);
 		expect(tryLockSpy).toHaveBeenCalledTimes(1);
 		expect(unlockSpy).toHaveBeenCalledTimes(1);
-		expect(federatedInstanceService.fetch).toHaveBeenCalledTimes(1);
+		expect(federatedInstanceService.fetchOrRegister).toHaveBeenCalledTimes(1);
 		expect(httpRequestService.getJson).toHaveBeenCalledTimes(0);
 	});
 
 	test('Do nothing when lock not acquired', async () => {
 		redisClient.set = mockRedis();
 		const now = Date.now();
-		federatedInstanceService.fetch.mockResolvedValue({ infoUpdatedAt: { getTime: () => now - 10 * 1000 * 60 * 60 * 24 } } as any);
+		federatedInstanceService.fetchOrRegister.mockResolvedValue({ infoUpdatedAt: { getTime: () => now - 10 * 1000 * 60 * 60 * 24 } } as any);
 		httpRequestService.getJson.mockImplementation(() => { throw Error(); });
 		await fetchInstanceMetadataService.tryLock('example.com');
 		const tryLockSpy = jest.spyOn(fetchInstanceMetadataService, 'tryLock');
@@ -114,14 +114,14 @@ describe('FetchInstanceMetadataService', () => {
 		await fetchInstanceMetadataService.fetchInstanceMetadata({ host: 'example.com' } as any);
 		expect(tryLockSpy).toHaveBeenCalledTimes(1);
 		expect(unlockSpy).toHaveBeenCalledTimes(0);
-		expect(federatedInstanceService.fetch).toHaveBeenCalledTimes(0);
+		expect(federatedInstanceService.fetchOrRegister).toHaveBeenCalledTimes(0);
 		expect(httpRequestService.getJson).toHaveBeenCalledTimes(0);
 	});
 
 	test('Do when lock not acquired but forced', async () => {
 		redisClient.set = mockRedis();
 		const now = Date.now();
-		federatedInstanceService.fetch.mockResolvedValue({ infoUpdatedAt: { getTime: () => now - 10 * 1000 * 60 * 60 * 24 } } as any);
+		federatedInstanceService.fetchOrRegister.mockResolvedValue({ infoUpdatedAt: { getTime: () => now - 10 * 1000 * 60 * 60 * 24 } } as any);
 		httpRequestService.getJson.mockImplementation(() => { throw Error(); });
 		await fetchInstanceMetadataService.tryLock('example.com');
 		const tryLockSpy = jest.spyOn(fetchInstanceMetadataService, 'tryLock');
@@ -130,7 +130,7 @@ describe('FetchInstanceMetadataService', () => {
 		await fetchInstanceMetadataService.fetchInstanceMetadata({ host: 'example.com' } as any, true);
 		expect(tryLockSpy).toHaveBeenCalledTimes(0);
 		expect(unlockSpy).toHaveBeenCalledTimes(1);
-		expect(federatedInstanceService.fetch).toHaveBeenCalledTimes(0);
+		expect(federatedInstanceService.fetchOrRegister).toHaveBeenCalledTimes(0);
 		expect(httpRequestService.getJson).toHaveBeenCalled();
 	});
 });
diff --git a/packages/frontend/src/pages/admin/performance.vue b/packages/frontend/src/pages/admin/performance.vue
index 7e0a932f82..12338f0bf9 100644
--- a/packages/frontend/src/pages/admin/performance.vue
+++ b/packages/frontend/src/pages/admin/performance.vue
@@ -29,6 +29,13 @@ SPDX-License-Identifier: AGPL-3.0-only
 				</MkSwitch>
 			</div>
 
+			<div class="_panel" style="padding: 16px;">
+				<MkSwitch v-model="enableStatsForFederatedInstances" @change="onChange_enableStatsForFederatedInstances">
+					<template #label>{{ i18n.ts.enableStatsForFederatedInstances }}</template>
+					<template #caption>{{ i18n.ts.turnOffToImprovePerformance }}</template>
+				</MkSwitch>
+			</div>
+
 			<div class="_panel" style="padding: 16px;">
 				<MkSwitch v-model="enableChartsForFederatedInstances" @change="onChange_enableChartsForFederatedInstances">
 					<template #label>{{ i18n.ts.enableChartsForFederatedInstances }}</template>
@@ -120,6 +127,7 @@ const meta = await misskeyApi('admin/meta');
 const enableServerMachineStats = ref(meta.enableServerMachineStats);
 const enableIdenticonGeneration = ref(meta.enableIdenticonGeneration);
 const enableChartsForRemoteUser = ref(meta.enableChartsForRemoteUser);
+const enableStatsForFederatedInstances = ref(meta.enableStatsForFederatedInstances);
 const enableChartsForFederatedInstances = ref(meta.enableChartsForFederatedInstances);
 
 function onChange_enableServerMachineStats(value: boolean) {
@@ -146,6 +154,14 @@ function onChange_enableChartsForRemoteUser(value: boolean) {
 	});
 }
 
+function onChange_enableStatsForFederatedInstances(value: boolean) {
+	os.apiWithDialog('admin/update-meta', {
+		enableStatsForFederatedInstances: value,
+	}).then(() => {
+		fetchInstance(true);
+	});
+}
+
 function onChange_enableChartsForFederatedInstances(value: boolean) {
 	os.apiWithDialog('admin/update-meta', {
 		enableChartsForFederatedInstances: value,
diff --git a/packages/misskey-js/src/autogen/types.ts b/packages/misskey-js/src/autogen/types.ts
index f61d72e280..6494614026 100644
--- a/packages/misskey-js/src/autogen/types.ts
+++ b/packages/misskey-js/src/autogen/types.ts
@@ -5165,6 +5165,7 @@ export type operations = {
             truemailAuthKey: string | null;
             enableChartsForRemoteUser: boolean;
             enableChartsForFederatedInstances: boolean;
+            enableStatsForFederatedInstances: boolean;
             enableServerMachineStats: boolean;
             enableIdenticonGeneration: boolean;
             manifestJsonOverride: string;
@@ -9547,6 +9548,7 @@ export type operations = {
           truemailAuthKey?: string | null;
           enableChartsForRemoteUser?: boolean;
           enableChartsForFederatedInstances?: boolean;
+          enableStatsForFederatedInstances?: boolean;
           enableServerMachineStats?: boolean;
           enableIdenticonGeneration?: boolean;
           serverRules?: string[];

From 5229f5de4d9ef7cd75d32466d29d672193adaf45 Mon Sep 17 00:00:00 2001
From: syuilo <4439005+syuilo@users.noreply.github.com>
Date: Sun, 13 Oct 2024 20:32:02 +0900
Subject: [PATCH 096/121] refactor(backend): remove unnecessary .then

---
 .../backend/src/core/AbuseReportNotificationService.ts   | 9 +++------
 packages/backend/src/core/AbuseReportService.ts          | 6 ++----
 packages/backend/src/core/SignupService.ts               | 4 ++--
 packages/backend/src/core/SystemWebhookService.ts        | 9 +++------
 4 files changed, 10 insertions(+), 18 deletions(-)

diff --git a/packages/backend/src/core/AbuseReportNotificationService.ts b/packages/backend/src/core/AbuseReportNotificationService.ts
index 7d030f2f16..25e265f2b1 100644
--- a/packages/backend/src/core/AbuseReportNotificationService.ts
+++ b/packages/backend/src/core/AbuseReportNotificationService.ts
@@ -288,8 +288,7 @@ export class AbuseReportNotificationService implements OnApplicationShutdown {
 			.log(updater, 'createAbuseReportNotificationRecipient', {
 				recipientId: id,
 				recipient: created,
-			})
-			.then();
+			});
 
 		return created;
 	}
@@ -327,8 +326,7 @@ export class AbuseReportNotificationService implements OnApplicationShutdown {
 				recipientId: params.id,
 				before: beforeEntity,
 				after: afterEntity,
-			})
-			.then();
+			});
 
 		return afterEntity;
 	}
@@ -349,8 +347,7 @@ export class AbuseReportNotificationService implements OnApplicationShutdown {
 			.log(updater, 'deleteAbuseReportNotificationRecipient', {
 				recipientId: id,
 				recipient: entity,
-			})
-			.then();
+			});
 	}
 
 	/**
diff --git a/packages/backend/src/core/AbuseReportService.ts b/packages/backend/src/core/AbuseReportService.ts
index 73baad5499..0b022d3b08 100644
--- a/packages/backend/src/core/AbuseReportService.ts
+++ b/packages/backend/src/core/AbuseReportService.ts
@@ -110,8 +110,7 @@ export class AbuseReportService {
 					reportId: report.id,
 					report: report,
 					resolvedAs: ps.resolvedAs,
-				})
-				.then();
+				});
 		}
 
 		return this.abuseUserReportsRepository.findBy({ id: In(reports.map(it => it.id)) })
@@ -148,8 +147,7 @@ export class AbuseReportService {
 			.log(moderator, 'forwardAbuseReport', {
 				reportId: report.id,
 				report: report,
-			})
-			.then();
+			});
 	}
 
 	@bindThis
diff --git a/packages/backend/src/core/SignupService.ts b/packages/backend/src/core/SignupService.ts
index cc8a3d6461..3865392b7f 100644
--- a/packages/backend/src/core/SignupService.ts
+++ b/packages/backend/src/core/SignupService.ts
@@ -150,8 +150,8 @@ export class SignupService {
 			}));
 		});
 
-		this.usersChart.update(account, true).then();
-		this.userService.notifySystemWebhook(account, 'userCreated').then();
+		this.usersChart.update(account, true);
+		this.userService.notifySystemWebhook(account, 'userCreated');
 
 		return { account, secret };
 	}
diff --git a/packages/backend/src/core/SystemWebhookService.ts b/packages/backend/src/core/SystemWebhookService.ts
index bb7c6b8c0e..db6407dcb3 100644
--- a/packages/backend/src/core/SystemWebhookService.ts
+++ b/packages/backend/src/core/SystemWebhookService.ts
@@ -101,8 +101,7 @@ export class SystemWebhookService implements OnApplicationShutdown {
 			.log(updater, 'createSystemWebhook', {
 				systemWebhookId: webhook.id,
 				webhook: webhook,
-			})
-			.then();
+			});
 
 		return webhook;
 	}
@@ -139,8 +138,7 @@ export class SystemWebhookService implements OnApplicationShutdown {
 				systemWebhookId: beforeEntity.id,
 				before: beforeEntity,
 				after: afterEntity,
-			})
-			.then();
+			});
 
 		return afterEntity;
 	}
@@ -158,8 +156,7 @@ export class SystemWebhookService implements OnApplicationShutdown {
 			.log(updater, 'deleteSystemWebhook', {
 				systemWebhookId: webhook.id,
 				webhook,
-			})
-			.then();
+			});
 	}
 
 	/**

From 33b34ad7b8248b4d5ddc37b986ffcf4dff6a37c4 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: Sun, 13 Oct 2024 20:32:12 +0900
Subject: [PATCH 097/121] =?UTF-8?q?feat:=20=E9=81=8B=E5=96=B6=E3=81=AE?=
 =?UTF-8?q?=E3=82=A2=E3=82=AF=E3=83=86=E3=82=A3=E3=83=93=E3=83=86=E3=82=A3?=
 =?UTF-8?q?=E3=81=8C=E4=B8=80=E5=AE=9A=E6=9C=9F=E9=96=93=E3=81=AA=E3=81=84?=
 =?UTF-8?q?=E5=A0=B4=E5=90=88=E3=81=AF=E9=80=9A=E7=9F=A5=EF=BC=8B=E6=8B=9B?=
 =?UTF-8?q?=E5=BE=85=E5=88=B6=E3=81=AB=E7=A7=BB=E8=A1=8C=E3=81=97=E3=81=9F?=
 =?UTF-8?q?=E9=9A=9B=E3=81=AB=E9=80=9A=E7=9F=A5=20(#14757)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

* feat: 運営のアクティビティが一定期間ない場合は通知+招待制に移行した際に通知

* fix misskey-js.api.md

* Revert "feat: 運営のアクティビティが一定期間ない場合は通知+招待制に移行した際に通知"

This reverts commit 3ab953bdf87f28411a1a10bce787a23d238cda80.

* 通知をやめてユーザ単位でのお知らせ機能に変更

* テスト用実装を戻す

* Update packages/backend/src/queue/processors/CheckModeratorsActivityProcessorService.ts

Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com>

* fix remove empty then

---------

Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com>
---
 locales/index.d.ts                            |   8 +
 locales/ja-JP.yml                             |   2 +
 .../backend/src/core/WebhookTestService.ts    |  17 ++
 packages/backend/src/models/SystemWebhook.ts  |   4 +
 ...CheckModeratorsActivityProcessorService.ts | 191 ++++++++++++++++--
 ...CheckModeratorsActivityProcessorService.ts | 168 +++++++++++++--
 .../src/components/MkSystemWebhookEditor.vue  |  18 ++
 packages/misskey-js/src/autogen/types.ts      |  10 +-
 8 files changed, 388 insertions(+), 30 deletions(-)

diff --git a/locales/index.d.ts b/locales/index.d.ts
index 3ffb67f31c..b5af5909a3 100644
--- a/locales/index.d.ts
+++ b/locales/index.d.ts
@@ -9661,6 +9661,14 @@ export interface Locale extends ILocale {
              * ユーザーが作成されたとき
              */
             "userCreated": string;
+            /**
+             * モデレーターが一定期間非アクティブになったとき
+             */
+            "inactiveModeratorsWarning": string;
+            /**
+             * モデレーターが一定期間非アクティブだったため、システムにより招待制へと変更されたとき
+             */
+            "inactiveModeratorsInvitationOnlyChanged": string;
         };
         /**
          * Webhookを削除しますか?
diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml
index 919be1e3ec..c448d4d50a 100644
--- a/locales/ja-JP.yml
+++ b/locales/ja-JP.yml
@@ -2559,6 +2559,8 @@ _webhookSettings:
     abuseReport: "ユーザーから通報があったとき"
     abuseReportResolved: "ユーザーからの通報を処理したとき"
     userCreated: "ユーザーが作成されたとき"
+    inactiveModeratorsWarning: "モデレーターが一定期間非アクティブになったとき"
+    inactiveModeratorsInvitationOnlyChanged: "モデレーターが一定期間非アクティブだったため、システムにより招待制へと変更されたとき"
   deleteConfirm: "Webhookを削除しますか?"
   testRemarks: "スイッチの右にあるボタンをクリックするとダミーのデータを使用したテスト用Webhookを送信できます。"
 
diff --git a/packages/backend/src/core/WebhookTestService.ts b/packages/backend/src/core/WebhookTestService.ts
index 4c45b95a64..55c8a52705 100644
--- a/packages/backend/src/core/WebhookTestService.ts
+++ b/packages/backend/src/core/WebhookTestService.ts
@@ -12,6 +12,7 @@ import { Packed } from '@/misc/json-schema.js';
 import { type WebhookEventTypes } from '@/models/Webhook.js';
 import { UserWebhookService } from '@/core/UserWebhookService.js';
 import { QueueService } from '@/core/QueueService.js';
+import { ModeratorInactivityRemainingTime } from '@/queue/processors/CheckModeratorsActivityProcessorService.js';
 
 const oneDayMillis = 24 * 60 * 60 * 1000;
 
@@ -446,6 +447,22 @@ export class WebhookTestService {
 				send(toPackedUserLite(dummyUser1));
 				break;
 			}
+			case 'inactiveModeratorsWarning': {
+				const dummyTime: ModeratorInactivityRemainingTime = {
+					time: 100000,
+					asDays: 1,
+					asHours: 24,
+				};
+
+				send({
+					remainingTime: dummyTime,
+				});
+				break;
+			}
+			case 'inactiveModeratorsInvitationOnlyChanged': {
+				send({});
+				break;
+			}
 		}
 	}
 }
diff --git a/packages/backend/src/models/SystemWebhook.ts b/packages/backend/src/models/SystemWebhook.ts
index d6c27eae51..1a7ce4962b 100644
--- a/packages/backend/src/models/SystemWebhook.ts
+++ b/packages/backend/src/models/SystemWebhook.ts
@@ -14,6 +14,10 @@ export const systemWebhookEventTypes = [
 	'abuseReportResolved',
 	// ユーザが作成された時
 	'userCreated',
+	// モデレータが一定期間不在である警告
+	'inactiveModeratorsWarning',
+	// モデレータが一定期間不在のためシステムにより招待制へと変更された
+	'inactiveModeratorsInvitationOnlyChanged',
 ] as const;
 export type SystemWebhookEventType = typeof systemWebhookEventTypes[number];
 
diff --git a/packages/backend/src/queue/processors/CheckModeratorsActivityProcessorService.ts b/packages/backend/src/queue/processors/CheckModeratorsActivityProcessorService.ts
index f2677f8e5c..87183cb342 100644
--- a/packages/backend/src/queue/processors/CheckModeratorsActivityProcessorService.ts
+++ b/packages/backend/src/queue/processors/CheckModeratorsActivityProcessorService.ts
@@ -3,24 +3,110 @@
  * SPDX-License-Identifier: AGPL-3.0-only
  */
 
-import { Injectable } from '@nestjs/common';
+import { Inject, Injectable } from '@nestjs/common';
+import { In } from 'typeorm';
 import type Logger from '@/logger.js';
 import { bindThis } from '@/decorators.js';
 import { MetaService } from '@/core/MetaService.js';
 import { RoleService } from '@/core/RoleService.js';
+import { EmailService } from '@/core/EmailService.js';
+import { MiUser, type UserProfilesRepository } from '@/models/_.js';
+import { DI } from '@/di-symbols.js';
+import { SystemWebhookService } from '@/core/SystemWebhookService.js';
+import { AnnouncementService } from '@/core/AnnouncementService.js';
 import { QueueLoggerService } from '../QueueLoggerService.js';
 
 // モデレーターが不在と判断する日付の閾値
 const MODERATOR_INACTIVITY_LIMIT_DAYS = 7;
-const ONE_DAY_MILLI_SEC = 1000 * 60 * 60 * 24;
+// 警告通知やログ出力を行う残日数の閾値
+const MODERATOR_INACTIVITY_WARNING_REMAINING_DAYS = 2;
+// 期限から6時間ごとに通知を行う
+const MODERATOR_INACTIVITY_WARNING_NOTIFY_INTERVAL_HOURS = 6;
+const ONE_HOUR_MILLI_SEC = 1000 * 60 * 60;
+const ONE_DAY_MILLI_SEC = ONE_HOUR_MILLI_SEC * 24;
+
+export type ModeratorInactivityEvaluationResult = {
+	isModeratorsInactive: boolean;
+	inactiveModerators: MiUser[];
+	remainingTime: ModeratorInactivityRemainingTime;
+}
+
+export type ModeratorInactivityRemainingTime = {
+	time: number;
+	asHours: number;
+	asDays: number;
+};
+
+function generateModeratorInactivityMail(remainingTime: ModeratorInactivityRemainingTime) {
+	const subject = 'Moderator Inactivity Warning / モデレーター不在の通知';
+
+	const timeVariant = remainingTime.asDays === 0 ? `${remainingTime.asHours} hours` : `${remainingTime.asDays} days`;
+	const timeVariantJa = remainingTime.asDays === 0 ? `${remainingTime.asHours} 時間` : `${remainingTime.asDays} 日間`;
+	const message = [
+		'To Moderators,',
+		'',
+		`A moderator has been inactive for a period of time. If there are ${timeVariant} of inactivity left, it will switch to invitation only.`,
+		'If you do not wish to move to invitation only, you must log into Misskey and update your last active date and time.',
+		'',
+		'---------------',
+		'',
+		'To モデレーター各位',
+		'',
+		`モデレーターが一定期間活動していないようです。あと${timeVariantJa}活動していない状態が続くと招待制に切り替わります。`,
+		'招待制に切り替わることを望まない場合は、Misskeyにログインして最終アクティブ日時を更新してください。',
+		'',
+	];
+
+	const html = message.join('<br>');
+	const text = message.join('\n');
+
+	return {
+		subject,
+		html,
+		text,
+	};
+}
+
+function generateInvitationOnlyChangedMail() {
+	const subject = 'Change to Invitation-Only / 招待制に変更されました';
+
+	const message = [
+		'To Moderators,',
+		'',
+		`Changed to invitation only because no moderator activity was detected for ${MODERATOR_INACTIVITY_LIMIT_DAYS} days.`,
+		'To cancel the invitation only, you need to access the control panel.',
+		'',
+		'---------------',
+		'',
+		'To モデレーター各位',
+		'',
+		`モデレーターの活動が${MODERATOR_INACTIVITY_LIMIT_DAYS}日間検出されなかったため、招待制に変更されました。`,
+		'招待制を解除するには、コントロールパネルにアクセスする必要があります。',
+		'',
+	];
+
+	const html = message.join('<br>');
+	const text = message.join('\n');
+
+	return {
+		subject,
+		html,
+		text,
+	};
+}
 
 @Injectable()
 export class CheckModeratorsActivityProcessorService {
 	private logger: Logger;
 
 	constructor(
+		@Inject(DI.userProfilesRepository)
+		private userProfilesRepository: UserProfilesRepository,
 		private metaService: MetaService,
 		private roleService: RoleService,
+		private emailService: EmailService,
+		private announcementService: AnnouncementService,
+		private systemWebhookService: SystemWebhookService,
 		private queueLoggerService: QueueLoggerService,
 	) {
 		this.logger = this.queueLoggerService.logger.createSubLogger('check-moderators-activity');
@@ -42,18 +128,23 @@ export class CheckModeratorsActivityProcessorService {
 
 	@bindThis
 	private async processImpl() {
-		const { isModeratorsInactive, inactivityLimitCountdown } = await this.evaluateModeratorsInactiveDays();
-		if (isModeratorsInactive) {
+		const evaluateResult = await this.evaluateModeratorsInactiveDays();
+		if (evaluateResult.isModeratorsInactive) {
 			this.logger.warn(`The moderator has been inactive for ${MODERATOR_INACTIVITY_LIMIT_DAYS} days. We will move to invitation only.`);
+
 			await this.changeToInvitationOnly();
-
-			// TODO: モデレータに通知メール+Misskey通知
-			// TODO: SystemWebhook通知
+			await this.notifyChangeToInvitationOnly();
 		} else {
-			if (inactivityLimitCountdown <= 2) {
-				this.logger.warn(`A moderator has been inactive for a period of time. If you are inactive for an additional ${inactivityLimitCountdown} days, it will switch to invitation only.`);
+			const remainingTime = evaluateResult.remainingTime;
+			if (remainingTime.asDays <= MODERATOR_INACTIVITY_WARNING_REMAINING_DAYS) {
+				const timeVariant = remainingTime.asDays === 0 ? `${remainingTime.asHours} hours` : `${remainingTime.asDays} days`;
+				this.logger.warn(`A moderator has been inactive for a period of time. If you are inactive for an additional ${timeVariant}, it will switch to invitation only.`);
 
-				// TODO: 警告メール
+				if (remainingTime.asHours % MODERATOR_INACTIVITY_WARNING_NOTIFY_INTERVAL_HOURS === 0) {
+					// ジョブの実行頻度と同等だと通知が多すぎるため期限から6時間ごとに通知する
+					// つまり、のこり2日を切ったら6時間ごとに通知が送られる
+					await this.notifyInactiveModeratorsWarning(remainingTime);
+				}
 			}
 		}
 	}
@@ -87,7 +178,7 @@ export class CheckModeratorsActivityProcessorService {
 	 * この場合、モデレータA, B, Cのアクティビティは判定基準日よりも古いため、モデレーターが不在と判断される。
 	 */
 	@bindThis
-	public async evaluateModeratorsInactiveDays() {
+	public async evaluateModeratorsInactiveDays(): Promise<ModeratorInactivityEvaluationResult> {
 		const today = new Date();
 		const inactivePeriod = new Date(today);
 		inactivePeriod.setDate(today.getDate() - MODERATOR_INACTIVITY_LIMIT_DAYS);
@@ -101,12 +192,18 @@ export class CheckModeratorsActivityProcessorService {
 		// 残りの猶予を示したいので、最終アクティブ日時が一番若いモデレータの日数を基準に猶予を計算する
 		// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
 		const newestLastActiveDate = new Date(Math.max(...moderators.map(it => it.lastActiveDate!.getTime())));
-		const inactivityLimitCountdown = Math.floor((newestLastActiveDate.getTime() - inactivePeriod.getTime()) / ONE_DAY_MILLI_SEC);
+		const remainingTime = newestLastActiveDate.getTime() - inactivePeriod.getTime();
+		const remainingTimeAsDays = Math.floor(remainingTime / ONE_DAY_MILLI_SEC);
+		const remainingTimeAsHours = Math.floor((remainingTime / ONE_HOUR_MILLI_SEC));
 
 		return {
 			isModeratorsInactive: inactiveModerators.length === moderators.length,
 			inactiveModerators,
-			inactivityLimitCountdown,
+			remainingTime: {
+				time: remainingTime,
+				asHours: remainingTimeAsHours,
+				asDays: remainingTimeAsDays,
+			},
 		};
 	}
 
@@ -115,6 +212,74 @@ export class CheckModeratorsActivityProcessorService {
 		await this.metaService.update({ disableRegistration: true });
 	}
 
+	@bindThis
+	public async notifyInactiveModeratorsWarning(remainingTime: ModeratorInactivityRemainingTime) {
+		// -- モデレータへのメール送信
+
+		const moderators = await this.fetchModerators();
+		const moderatorProfiles = await this.userProfilesRepository
+			.findBy({ userId: In(moderators.map(it => it.id)) })
+			.then(it => new Map(it.map(it => [it.userId, it])));
+
+		const mail = generateModeratorInactivityMail(remainingTime);
+		for (const moderator of moderators) {
+			const profile = moderatorProfiles.get(moderator.id);
+			if (profile && profile.email && profile.emailVerified) {
+				this.emailService.sendEmail(profile.email, mail.subject, mail.html, mail.text);
+			}
+		}
+
+		// -- SystemWebhook
+
+		const systemWebhooks = await this.systemWebhookService.fetchActiveSystemWebhooks()
+			.then(it => it.filter(it => it.on.includes('inactiveModeratorsWarning')));
+		for (const systemWebhook of systemWebhooks) {
+			this.systemWebhookService.enqueueSystemWebhook(
+				systemWebhook,
+				'inactiveModeratorsWarning',
+				{ remainingTime: remainingTime },
+			);
+		}
+	}
+
+	@bindThis
+	public async notifyChangeToInvitationOnly() {
+		// -- モデレータへのメールとお知らせ(個人向け)送信
+
+		const moderators = await this.fetchModerators();
+		const moderatorProfiles = await this.userProfilesRepository
+			.findBy({ userId: In(moderators.map(it => it.id)) })
+			.then(it => new Map(it.map(it => [it.userId, it])));
+
+		const mail = generateInvitationOnlyChangedMail();
+		for (const moderator of moderators) {
+			this.announcementService.create({
+				title: mail.subject,
+				text: mail.text,
+				forExistingUsers: true,
+				needConfirmationToRead: true,
+				userId: moderator.id,
+			});
+
+			const profile = moderatorProfiles.get(moderator.id);
+			if (profile && profile.email && profile.emailVerified) {
+				this.emailService.sendEmail(profile.email, mail.subject, mail.html, mail.text);
+			}
+		}
+
+		// -- SystemWebhook
+
+		const systemWebhooks = await this.systemWebhookService.fetchActiveSystemWebhooks()
+			.then(it => it.filter(it => it.on.includes('inactiveModeratorsInvitationOnlyChanged')));
+		for (const systemWebhook of systemWebhooks) {
+			this.systemWebhookService.enqueueSystemWebhook(
+				systemWebhook,
+				'inactiveModeratorsInvitationOnlyChanged',
+				{},
+			);
+		}
+	}
+
 	@bindThis
 	private async fetchModerators() {
 		// TODO: モデレーター以外にも特別な権限を持つユーザーがいる場合は考慮する
diff --git a/packages/backend/test/unit/queue/processors/CheckModeratorsActivityProcessorService.ts b/packages/backend/test/unit/queue/processors/CheckModeratorsActivityProcessorService.ts
index b783320aa0..1506283a3c 100644
--- a/packages/backend/test/unit/queue/processors/CheckModeratorsActivityProcessorService.ts
+++ b/packages/backend/test/unit/queue/processors/CheckModeratorsActivityProcessorService.ts
@@ -8,13 +8,16 @@ import { Test, TestingModule } from '@nestjs/testing';
 import * as lolex from '@sinonjs/fake-timers';
 import { addHours, addSeconds, subDays, subHours, subSeconds } from 'date-fns';
 import { CheckModeratorsActivityProcessorService } from '@/queue/processors/CheckModeratorsActivityProcessorService.js';
-import { MiUser, UserProfilesRepository, UsersRepository } from '@/models/_.js';
+import { MiSystemWebhook, MiUser, MiUserProfile, UserProfilesRepository, UsersRepository } from '@/models/_.js';
 import { IdService } from '@/core/IdService.js';
 import { RoleService } from '@/core/RoleService.js';
 import { GlobalModule } from '@/GlobalModule.js';
 import { MetaService } from '@/core/MetaService.js';
 import { DI } from '@/di-symbols.js';
 import { QueueLoggerService } from '@/queue/QueueLoggerService.js';
+import { EmailService } from '@/core/EmailService.js';
+import { SystemWebhookService } from '@/core/SystemWebhookService.js';
+import { AnnouncementService } from '@/core/AnnouncementService.js';
 
 const baseDate = new Date(Date.UTC(2000, 11, 15, 12, 0, 0));
 
@@ -29,10 +32,17 @@ describe('CheckModeratorsActivityProcessorService', () => {
 	let userProfilesRepository: UserProfilesRepository;
 	let idService: IdService;
 	let roleService: jest.Mocked<RoleService>;
+	let announcementService: jest.Mocked<AnnouncementService>;
+	let emailService: jest.Mocked<EmailService>;
+	let systemWebhookService: jest.Mocked<SystemWebhookService>;
+
+	let systemWebhook1: MiSystemWebhook;
+	let systemWebhook2: MiSystemWebhook;
+	let systemWebhook3: MiSystemWebhook;
 
 	// --------------------------------------------------------------------------------------
 
-	async function createUser(data: Partial<MiUser> = {}) {
+	async function createUser(data: Partial<MiUser> = {}, profile: Partial<MiUserProfile> = {}): Promise<MiUser> {
 		const id = idService.gen();
 		const user = await usersRepository
 			.insert({
@@ -45,11 +55,27 @@ describe('CheckModeratorsActivityProcessorService', () => {
 
 		await userProfilesRepository.insert({
 			userId: user.id,
+			...profile,
 		});
 
 		return user;
 	}
 
+	function crateSystemWebhook(data: Partial<MiSystemWebhook> = {}): MiSystemWebhook {
+		return {
+			id: idService.gen(),
+			isActive: true,
+			updatedAt: new Date(),
+			latestSentAt: null,
+			latestStatus: null,
+			name: 'test',
+			url: 'https://example.com',
+			secret: 'test',
+			on: [],
+			...data,
+		};
+	}
+
 	function mockModeratorRole(users: MiUser[]) {
 		roleService.getModerators.mockReset();
 		roleService.getModerators.mockResolvedValue(users);
@@ -72,6 +98,18 @@ describe('CheckModeratorsActivityProcessorService', () => {
 					{
 						provide: MetaService, useFactory: () => ({ fetch: jest.fn() }),
 					},
+					{
+						provide: AnnouncementService, useFactory: () => ({ create: jest.fn() }),
+					},
+					{
+						provide: EmailService, useFactory: () => ({ sendEmail: jest.fn() }),
+					},
+					{
+						provide: SystemWebhookService, useFactory: () => ({
+							fetchActiveSystemWebhooks: jest.fn(),
+							enqueueSystemWebhook: jest.fn(),
+						}),
+					},
 					{
 						provide: QueueLoggerService, useFactory: () => ({
 							logger: ({
@@ -93,6 +131,9 @@ describe('CheckModeratorsActivityProcessorService', () => {
 		service = app.get(CheckModeratorsActivityProcessorService);
 		idService = app.get(IdService);
 		roleService = app.get(RoleService) as jest.Mocked<RoleService>;
+		announcementService = app.get(AnnouncementService) as jest.Mocked<AnnouncementService>;
+		emailService = app.get(EmailService) as jest.Mocked<EmailService>;
+		systemWebhookService = app.get(SystemWebhookService) as jest.Mocked<SystemWebhookService>;
 
 		app.enableShutdownHooks();
 	});
@@ -102,6 +143,15 @@ describe('CheckModeratorsActivityProcessorService', () => {
 			now: new Date(baseDate),
 			shouldClearNativeTimers: true,
 		});
+
+		systemWebhook1 = crateSystemWebhook({ on: ['inactiveModeratorsWarning'] });
+		systemWebhook2 = crateSystemWebhook({ on: ['inactiveModeratorsWarning', 'inactiveModeratorsInvitationOnlyChanged'] });
+		systemWebhook3 = crateSystemWebhook({ on: ['abuseReport'] });
+
+		emailService.sendEmail.mockReturnValue(Promise.resolve());
+		announcementService.create.mockReturnValue(Promise.resolve({} as never));
+		systemWebhookService.fetchActiveSystemWebhooks.mockResolvedValue([systemWebhook1, systemWebhook2, systemWebhook3]);
+		systemWebhookService.enqueueSystemWebhook.mockReturnValue(Promise.resolve({} as never));
 	});
 
 	afterEach(async () => {
@@ -109,6 +159,9 @@ describe('CheckModeratorsActivityProcessorService', () => {
 		await usersRepository.delete({});
 		await userProfilesRepository.delete({});
 		roleService.getModerators.mockReset();
+		announcementService.create.mockReset();
+		emailService.sendEmail.mockReset();
+		systemWebhookService.enqueueSystemWebhook.mockReset();
 	});
 
 	afterAll(async () => {
@@ -152,7 +205,7 @@ describe('CheckModeratorsActivityProcessorService', () => {
 			expect(result.inactiveModerators).toEqual([user1]);
 		});
 
-		test('[countdown] 猶予まで24時間ある場合、猶予1日として計算される', async () => {
+		test('[remainingTime] 猶予まで24時間ある場合、猶予1日として計算される', async () => {
 			const [user1, user2] = await Promise.all([
 				createUser({ lastActiveDate: subDays(baseDate, 8) }),
 				// 猶予はこのユーザ基準で計算される想定。
@@ -165,10 +218,11 @@ describe('CheckModeratorsActivityProcessorService', () => {
 			const result = await service.evaluateModeratorsInactiveDays();
 			expect(result.isModeratorsInactive).toBe(false);
 			expect(result.inactiveModerators).toEqual([user1]);
-			expect(result.inactivityLimitCountdown).toBe(1);
+			expect(result.remainingTime.asDays).toBe(1);
+			expect(result.remainingTime.asHours).toBe(24);
 		});
 
-		test('[countdown] 猶予まで25時間ある場合、猶予1日として計算される', async () => {
+		test('[remainingTime] 猶予まで25時間ある場合、猶予1日として計算される', async () => {
 			const [user1, user2] = await Promise.all([
 				createUser({ lastActiveDate: subDays(baseDate, 8) }),
 				// 猶予はこのユーザ基準で計算される想定。
@@ -181,10 +235,11 @@ describe('CheckModeratorsActivityProcessorService', () => {
 			const result = await service.evaluateModeratorsInactiveDays();
 			expect(result.isModeratorsInactive).toBe(false);
 			expect(result.inactiveModerators).toEqual([user1]);
-			expect(result.inactivityLimitCountdown).toBe(1);
+			expect(result.remainingTime.asDays).toBe(1);
+			expect(result.remainingTime.asHours).toBe(25);
 		});
 
-		test('[countdown] 猶予まで23時間ある場合、猶予0日として計算される', async () => {
+		test('[remainingTime] 猶予まで23時間ある場合、猶予0日として計算される', async () => {
 			const [user1, user2] = await Promise.all([
 				createUser({ lastActiveDate: subDays(baseDate, 8) }),
 				// 猶予はこのユーザ基準で計算される想定。
@@ -197,10 +252,11 @@ describe('CheckModeratorsActivityProcessorService', () => {
 			const result = await service.evaluateModeratorsInactiveDays();
 			expect(result.isModeratorsInactive).toBe(false);
 			expect(result.inactiveModerators).toEqual([user1]);
-			expect(result.inactivityLimitCountdown).toBe(0);
+			expect(result.remainingTime.asDays).toBe(0);
+			expect(result.remainingTime.asHours).toBe(23);
 		});
 
-		test('[countdown] 期限ちょうどの場合、猶予0日として計算される', async () => {
+		test('[remainingTime] 期限ちょうどの場合、猶予0日として計算される', async () => {
 			const [user1, user2] = await Promise.all([
 				createUser({ lastActiveDate: subDays(baseDate, 8) }),
 				// 猶予はこのユーザ基準で計算される想定。
@@ -213,10 +269,11 @@ describe('CheckModeratorsActivityProcessorService', () => {
 			const result = await service.evaluateModeratorsInactiveDays();
 			expect(result.isModeratorsInactive).toBe(false);
 			expect(result.inactiveModerators).toEqual([user1]);
-			expect(result.inactivityLimitCountdown).toBe(0);
+			expect(result.remainingTime.asDays).toBe(0);
+			expect(result.remainingTime.asHours).toBe(0);
 		});
 
-		test('[countdown] 期限より1時間超過している場合、猶予-1日として計算される', async () => {
+		test('[remainingTime] 期限より1時間超過している場合、猶予-1日として計算される', async () => {
 			const [user1, user2] = await Promise.all([
 				createUser({ lastActiveDate: subDays(baseDate, 8) }),
 				// 猶予はこのユーザ基準で計算される想定。
@@ -229,7 +286,94 @@ describe('CheckModeratorsActivityProcessorService', () => {
 			const result = await service.evaluateModeratorsInactiveDays();
 			expect(result.isModeratorsInactive).toBe(true);
 			expect(result.inactiveModerators).toEqual([user1, user2]);
-			expect(result.inactivityLimitCountdown).toBe(-1);
+			expect(result.remainingTime.asDays).toBe(-1);
+			expect(result.remainingTime.asHours).toBe(-1);
+		});
+
+		test('[remainingTime] 期限より25時間超過している場合、猶予-2日として計算される', async () => {
+			const [user1, user2] = await Promise.all([
+				createUser({ lastActiveDate: subDays(baseDate, 10) }),
+				// 猶予はこのユーザ基準で計算される想定。
+				// 期限より1時間超過->猶予-1日として計算されるはずである
+				createUser({ lastActiveDate: subDays(subHours(baseDate, 25), 7) }),
+			]);
+
+			mockModeratorRole([user1, user2]);
+
+			const result = await service.evaluateModeratorsInactiveDays();
+			expect(result.isModeratorsInactive).toBe(true);
+			expect(result.inactiveModerators).toEqual([user1, user2]);
+			expect(result.remainingTime.asDays).toBe(-2);
+			expect(result.remainingTime.asHours).toBe(-25);
+		});
+	});
+
+	describe('notifyInactiveModeratorsWarning', () => {
+		test('[notification + mail] 通知はモデレータ全員に発信され、メールはメールアドレスが存在+認証済みの場合のみ', async () => {
+			const [user1, user2, user3, user4, root] = await Promise.all([
+				createUser({}, { email: 'user1@example.com', emailVerified: true }),
+				createUser({}, { email: 'user2@example.com', emailVerified: false }),
+				createUser({}, { email: null, emailVerified: false }),
+				createUser({}, { email: 'user4@example.com', emailVerified: true }),
+				createUser({ isRoot: true }, { email: 'root@example.com', emailVerified: true }),
+			]);
+
+			mockModeratorRole([user1, user2, user3, root]);
+			await service.notifyInactiveModeratorsWarning({ time: 1, asDays: 0, asHours: 0 });
+
+			expect(emailService.sendEmail).toHaveBeenCalledTimes(2);
+			expect(emailService.sendEmail.mock.calls[0][0]).toBe('user1@example.com');
+			expect(emailService.sendEmail.mock.calls[1][0]).toBe('root@example.com');
+		});
+
+		test('[systemWebhook] "inactiveModeratorsWarning"が有効なSystemWebhookに対して送信される', async () => {
+			const [user1] = await Promise.all([
+				createUser({}, { email: 'user1@example.com', emailVerified: true }),
+			]);
+
+			mockModeratorRole([user1]);
+			await service.notifyInactiveModeratorsWarning({ time: 1, asDays: 0, asHours: 0 });
+
+			expect(systemWebhookService.enqueueSystemWebhook).toHaveBeenCalledTimes(2);
+			expect(systemWebhookService.enqueueSystemWebhook.mock.calls[0][0]).toEqual(systemWebhook1);
+			expect(systemWebhookService.enqueueSystemWebhook.mock.calls[1][0]).toEqual(systemWebhook2);
+		});
+	});
+
+	describe('notifyChangeToInvitationOnly', () => {
+		test('[notification + mail] 通知はモデレータ全員に発信され、メールはメールアドレスが存在+認証済みの場合のみ', async () => {
+			const [user1, user2, user3, user4, root] = await Promise.all([
+				createUser({}, { email: 'user1@example.com', emailVerified: true }),
+				createUser({}, { email: 'user2@example.com', emailVerified: false }),
+				createUser({}, { email: null, emailVerified: false }),
+				createUser({}, { email: 'user4@example.com', emailVerified: true }),
+				createUser({ isRoot: true }, { email: 'root@example.com', emailVerified: true }),
+			]);
+
+			mockModeratorRole([user1, user2, user3, root]);
+			await service.notifyChangeToInvitationOnly();
+
+			expect(announcementService.create).toHaveBeenCalledTimes(4);
+			expect(announcementService.create.mock.calls[0][0].userId).toBe(user1.id);
+			expect(announcementService.create.mock.calls[1][0].userId).toBe(user2.id);
+			expect(announcementService.create.mock.calls[2][0].userId).toBe(user3.id);
+			expect(announcementService.create.mock.calls[3][0].userId).toBe(root.id);
+
+			expect(emailService.sendEmail).toHaveBeenCalledTimes(2);
+			expect(emailService.sendEmail.mock.calls[0][0]).toBe('user1@example.com');
+			expect(emailService.sendEmail.mock.calls[1][0]).toBe('root@example.com');
+		});
+
+		test('[systemWebhook] "inactiveModeratorsInvitationOnlyChanged"が有効なSystemWebhookに対して送信される', async () => {
+			const [user1] = await Promise.all([
+				createUser({}, { email: 'user1@example.com', emailVerified: true }),
+			]);
+
+			mockModeratorRole([user1]);
+			await service.notifyChangeToInvitationOnly();
+
+			expect(systemWebhookService.enqueueSystemWebhook).toHaveBeenCalledTimes(1);
+			expect(systemWebhookService.enqueueSystemWebhook.mock.calls[0][0]).toEqual(systemWebhook2);
 		});
 	});
 });
diff --git a/packages/frontend/src/components/MkSystemWebhookEditor.vue b/packages/frontend/src/components/MkSystemWebhookEditor.vue
index a00cf0d9d3..485d003f93 100644
--- a/packages/frontend/src/components/MkSystemWebhookEditor.vue
+++ b/packages/frontend/src/components/MkSystemWebhookEditor.vue
@@ -55,6 +55,18 @@ SPDX-License-Identifier: AGPL-3.0-only
 								</MkSwitch>
 								<MkButton v-show="mode === 'edit'" transparent :class="$style.testButton" :disabled="!(isActive && events.userCreated)" @click="test('userCreated')"><i class="ti ti-send"></i></MkButton>
 							</div>
+							<div :class="$style.switchBox">
+								<MkSwitch v-model="events.inactiveModeratorsWarning" :disabled="disabledEvents.inactiveModeratorsWarning">
+									<template #label>{{ i18n.ts._webhookSettings._systemEvents.inactiveModeratorsWarning }}</template>
+								</MkSwitch>
+								<MkButton v-show="mode === 'edit'" transparent :class="$style.testButton" :disabled="!(isActive && events.inactiveModeratorsWarning)" @click="test('inactiveModeratorsWarning')"><i class="ti ti-send"></i></MkButton>
+							</div>
+							<div :class="$style.switchBox">
+								<MkSwitch v-model="events.inactiveModeratorsInvitationOnlyChanged" :disabled="disabledEvents.inactiveModeratorsInvitationOnlyChanged">
+									<template #label>{{ i18n.ts._webhookSettings._systemEvents.inactiveModeratorsInvitationOnlyChanged }}</template>
+								</MkSwitch>
+								<MkButton v-show="mode === 'edit'" transparent :class="$style.testButton" :disabled="!(isActive && events.inactiveModeratorsInvitationOnlyChanged)" @click="test('inactiveModeratorsInvitationOnlyChanged')"><i class="ti ti-send"></i></MkButton>
+							</div>
 						</div>
 
 						<div v-show="mode === 'edit'" :class="$style.description">
@@ -100,6 +112,8 @@ type EventType = {
 	abuseReport: boolean;
 	abuseReportResolved: boolean;
 	userCreated: boolean;
+	inactiveModeratorsWarning: boolean;
+	inactiveModeratorsInvitationOnlyChanged: boolean;
 }
 
 const emit = defineEmits<{
@@ -123,6 +137,8 @@ const events = ref<EventType>({
 	abuseReport: true,
 	abuseReportResolved: true,
 	userCreated: true,
+	inactiveModeratorsWarning: true,
+	inactiveModeratorsInvitationOnlyChanged: true,
 });
 const isActive = ref<boolean>(true);
 
@@ -130,6 +146,8 @@ const disabledEvents = ref<EventType>({
 	abuseReport: false,
 	abuseReportResolved: false,
 	userCreated: false,
+	inactiveModeratorsWarning: false,
+	inactiveModeratorsInvitationOnlyChanged: false,
 });
 
 const disableSubmitButton = computed(() => {
diff --git a/packages/misskey-js/src/autogen/types.ts b/packages/misskey-js/src/autogen/types.ts
index 6494614026..698c08826a 100644
--- a/packages/misskey-js/src/autogen/types.ts
+++ b/packages/misskey-js/src/autogen/types.ts
@@ -5048,7 +5048,7 @@ export type components = {
       latestSentAt: string | null;
       latestStatus: number | null;
       name: string;
-      on: ('abuseReport' | 'abuseReportResolved' | 'userCreated')[];
+      on: ('abuseReport' | 'abuseReportResolved' | 'userCreated' | 'inactiveModeratorsWarning' | 'inactiveModeratorsInvitationOnlyChanged')[];
       url: string;
       secret: string;
     };
@@ -10249,7 +10249,7 @@ export type operations = {
         'application/json': {
           isActive: boolean;
           name: string;
-          on: ('abuseReport' | 'abuseReportResolved' | 'userCreated')[];
+          on: ('abuseReport' | 'abuseReportResolved' | 'userCreated' | 'inactiveModeratorsWarning' | 'inactiveModeratorsInvitationOnlyChanged')[];
           url: string;
           secret: string;
         };
@@ -10359,7 +10359,7 @@ export type operations = {
       content: {
         'application/json': {
           isActive?: boolean;
-          on?: ('abuseReport' | 'abuseReportResolved' | 'userCreated')[];
+          on?: ('abuseReport' | 'abuseReportResolved' | 'userCreated' | 'inactiveModeratorsWarning' | 'inactiveModeratorsInvitationOnlyChanged')[];
         };
       };
     };
@@ -10472,7 +10472,7 @@ export type operations = {
           id: string;
           isActive: boolean;
           name: string;
-          on: ('abuseReport' | 'abuseReportResolved' | 'userCreated')[];
+          on: ('abuseReport' | 'abuseReportResolved' | 'userCreated' | 'inactiveModeratorsWarning' | 'inactiveModeratorsInvitationOnlyChanged')[];
           url: string;
           secret: string;
         };
@@ -10531,7 +10531,7 @@ export type operations = {
           /** Format: misskey:id */
           webhookId: string;
           /** @enum {string} */
-          type: 'abuseReport' | 'abuseReportResolved' | 'userCreated';
+          type: 'abuseReport' | 'abuseReportResolved' | 'userCreated' | 'inactiveModeratorsWarning' | 'inactiveModeratorsInvitationOnlyChanged';
           override?: {
             url?: string;
             secret?: string;

From fb23b24f5cbaca3f7a1ad4ab4893802cbf7c3e53 Mon Sep 17 00:00:00 2001
From: "github-actions[bot]" <github-actions[bot]@users.noreply.github.com>
Date: Sun, 13 Oct 2024 11:43:27 +0000
Subject: [PATCH 098/121] Bump version to 2024.10.1-beta.4

---
 package.json                     | 2 +-
 packages/misskey-js/package.json | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/package.json b/package.json
index 2c84c55303..4b477aba4b 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
 	"name": "misskey",
-	"version": "2024.10.1-beta.3",
+	"version": "2024.10.1-beta.4",
 	"codename": "nasubi",
 	"repository": {
 		"type": "git",
diff --git a/packages/misskey-js/package.json b/packages/misskey-js/package.json
index e5f4b7b9dd..a59385dc10 100644
--- a/packages/misskey-js/package.json
+++ b/packages/misskey-js/package.json
@@ -1,7 +1,7 @@
 {
 	"type": "module",
 	"name": "misskey-js",
-	"version": "2024.10.1-beta.3",
+	"version": "2024.10.1-beta.4",
 	"description": "Misskey SDK for JavaScript",
 	"license": "MIT",
 	"main": "./built/index.js",

From 088e05ea66f0bf9442f2d4d9772958dfe8e76d8b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=8B=E3=81=A3=E3=81=93=E3=81=8B=E3=82=8A?=
 <67428053+kakkokari-gtyih@users.noreply.github.com>
Date: Mon, 14 Oct 2024 02:54:01 +0900
Subject: [PATCH 099/121] =?UTF-8?q?fix(frontend):=20=E4=BD=BF=E7=94=A8?=
 =?UTF-8?q?=E3=81=95=E3=82=8C=E3=81=A6=E3=81=84=E3=82=8Bexpose=E3=82=92?=
 =?UTF-8?q?=E5=BE=A9=E6=B4=BB=E3=81=95=E3=81=9B=E3=82=8B=20(#14764)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../src/components/global/MkStickyContainer.vue     | 13 +++++++++----
 1 file changed, 9 insertions(+), 4 deletions(-)

diff --git a/packages/frontend/src/components/global/MkStickyContainer.vue b/packages/frontend/src/components/global/MkStickyContainer.vue
index 2763ecadd6..1aebf487bb 100644
--- a/packages/frontend/src/components/global/MkStickyContainer.vue
+++ b/packages/frontend/src/components/global/MkStickyContainer.vue
@@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 -->
 
 <template>
-<div>
+<div ref="rootEl">
 	<div ref="headerEl" :class="$style.header">
 		<slot name="header"></slot>
 	</div>
@@ -22,12 +22,13 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { onMounted, onUnmounted, provide, inject, Ref, ref, watch, shallowRef } from 'vue';
+import { onMounted, onUnmounted, provide, inject, Ref, ref, watch, useTemplateRef } from 'vue';
 
 import { CURRENT_STICKY_BOTTOM, CURRENT_STICKY_TOP } from '@@/js/const.js';
 
-const headerEl = shallowRef<HTMLElement>();
-const footerEl = shallowRef<HTMLElement>();
+const rootEl = useTemplateRef('rootEl');
+const headerEl = useTemplateRef('headerEl');
+const footerEl = useTemplateRef('footerEl');
 
 const headerHeight = ref<string | undefined>();
 const childStickyTop = ref(0);
@@ -76,6 +77,10 @@ onMounted(() => {
 onUnmounted(() => {
 	observer.disconnect();
 });
+
+defineExpose({
+	rootEl,
+});
 </script>
 
 <style lang='scss' module>

From d0bb0b51f5ec8a9125ee768d75a1e8a9f76c6849 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=8B=E3=81=A3=E3=81=93=E3=81=8B=E3=82=8A?=
 <67428053+kakkokari-gtyih@users.noreply.github.com>
Date: Mon, 14 Oct 2024 03:06:10 +0900
Subject: [PATCH 100/121] =?UTF-8?q?fix(frontend):=20=E3=82=BF=E3=82=A4?=
 =?UTF-8?q?=E3=83=A0=E3=83=A9=E3=82=A4=E3=83=B3=E3=81=A7=E3=80=81=E5=BA=83?=
 =?UTF-8?q?=E5=91=8A=E3=81=8C=E3=81=AA=E3=81=84=E9=9A=9B=E3=81=AB=E3=82=82?=
 =?UTF-8?q?=E5=BA=83=E5=91=8A=E3=81=AEwrapper=E3=81=8C=E5=87=BA=E3=81=A6?=
 =?UTF-8?q?=E3=81=97=E3=81=BE=E3=81=86=E3=81=AE=E3=82=92=E4=BF=AE=E6=AD=A3?=
 =?UTF-8?q?=20(#14763)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../frontend/src/components/MkDateSeparatedList.vue   | 11 +++++++++--
 1 file changed, 9 insertions(+), 2 deletions(-)

diff --git a/packages/frontend/src/components/MkDateSeparatedList.vue b/packages/frontend/src/components/MkDateSeparatedList.vue
index 5976aa02f5..f04e5cf7c6 100644
--- a/packages/frontend/src/components/MkDateSeparatedList.vue
+++ b/packages/frontend/src/components/MkDateSeparatedList.vue
@@ -9,6 +9,7 @@ import MkAd from '@/components/global/MkAd.vue';
 import { isDebuggerEnabled, stackTraceInstances } from '@/debug.js';
 import { i18n } from '@/i18n.js';
 import * as os from '@/os.js';
+import { instance } from '@/instance.js';
 import { defaultStore } from '@/store.js';
 import { MisskeyEntity } from '@/types/date-separated-list.js';
 
@@ -99,10 +100,10 @@ export default defineComponent({
 
 				return [el, separator];
 			} else {
-				if (props.ad && item._shouldInsertAd_) {
+				if (props.ad && instance.ads.length > 0 && item._shouldInsertAd_) {
 					return [h('div', {
 						key: item.id + ':ad',
-						style: 'padding: 8px; background-size: auto auto; background-image: repeating-linear-gradient(45deg, transparent, transparent 8px, var(--MI_THEME-bg) 8px, var(--MI_THEME-bg) 14px );',
+						class: $style['ad-wrapper'],
 					}, [h(MkAd, {
 						prefer: ['horizontal', 'horizontal-big'],
 					})]), el];
@@ -255,5 +256,11 @@ export default defineComponent({
 .date-2-icon {
 	margin-left: 8px;
 }
+
+.ad-wrapper {
+	padding: 8px;
+	background-size: auto auto;
+	background-image: repeating-linear-gradient(45deg, transparent, transparent 8px, var(--MI_THEME-bg) 8px, var(--MI_THEME-bg) 14px);
+}
 </style>
 

From 064d6ca56f66ff3061fb27897df429e534288462 Mon Sep 17 00:00:00 2001
From: syuilo <4439005+syuilo@users.noreply.github.com>
Date: Mon, 14 Oct 2024 09:11:03 +0900
Subject: [PATCH 101/121] =?UTF-8?q?fix(backend):=20RBT=E6=9C=89=E5=8A=B9?=
 =?UTF-8?q?=E6=99=82=E3=80=81=E3=83=AA=E3=83=8E=E3=83=BC=E3=83=88=E3=81=AE?=
 =?UTF-8?q?=E3=83=AA=E3=82=A2=E3=82=AF=E3=82=B7=E3=83=A7=E3=83=B3=E3=81=8C?=
 =?UTF-8?q?=E5=8F=8D=E6=98=A0=E3=81=95=E3=82=8C=E3=81=AA=E3=81=84=E5=95=8F?=
 =?UTF-8?q?=E9=A1=8C=E3=82=92=E4=BF=AE=E6=AD=A3?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 CHANGELOG.md                                  |  1 +
 .../src/core/entities/NoteEntityService.ts    | 27 +++++++++++++++++--
 2 files changed, 26 insertions(+), 2 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 130eb00b77..22b5506f28 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -14,6 +14,7 @@
 ### Server
 - Feat: モデレータ権限を持つユーザが全員7日間活動しなかった場合は自動的に招待制へと移行するように ( #13437 )
 - Fix: `admin/emoji/update`エンドポイントのidのみ指定した時不正なエラーが発生するバグを修正
+- Fix: RBT有効時、リノートのリアクションが反映されない問題を修正
 
 ### Server
 - Fix: キューのエラーログを簡略化するように  
diff --git a/packages/backend/src/core/entities/NoteEntityService.ts b/packages/backend/src/core/entities/NoteEntityService.ts
index c64e9151a7..e530772dd9 100644
--- a/packages/backend/src/core/entities/NoteEntityService.ts
+++ b/packages/backend/src/core/entities/NoteEntityService.ts
@@ -22,6 +22,29 @@ import type { ReactionService } from '../ReactionService.js';
 import type { UserEntityService } from './UserEntityService.js';
 import type { DriveFileEntityService } from './DriveFileEntityService.js';
 
+function isPureRenote(note: MiNote): note is MiNote & { renoteId: MiNote['id']; renote: MiNote } {
+	return (
+		note.renote != null &&
+		note.reply == null &&
+		note.text == null &&
+		note.cw == null &&
+		(note.fileIds == null || note.fileIds.length === 0) &&
+		!note.hasPoll
+	);
+}
+
+function getAppearNoteIds(notes: MiNote[]): Set<string> {
+	const appearNoteIds = new Set<string>();
+	for (const note of notes) {
+		if (isPureRenote(note)) {
+			appearNoteIds.add(note.renoteId);
+		} else {
+			appearNoteIds.add(note.id);
+		}
+	}
+	return appearNoteIds;
+}
+
 @Injectable()
 export class NoteEntityService implements OnModuleInit {
 	private userEntityService: UserEntityService;
@@ -421,7 +444,7 @@ export class NoteEntityService implements OnModuleInit {
 	) {
 		if (notes.length === 0) return [];
 
-		const bufferedReactions = this.meta.enableReactionsBuffering ? await this.reactionsBufferingService.getMany(notes.map(x => x.id)) : null;
+		const bufferedReactions = this.meta.enableReactionsBuffering ? await this.reactionsBufferingService.getMany([...getAppearNoteIds(notes)]) : null;
 
 		const meId = me ? me.id : null;
 		const myReactionsMap = new Map<MiNote['id'], string | null>();
@@ -432,7 +455,7 @@ export class NoteEntityService implements OnModuleInit {
 			const oldId = this.idService.gen(Date.now() - 2000);
 
 			for (const note of notes) {
-				if (note.renote && (note.text == null && note.fileIds.length === 0)) { // pure renote
+				if (isPureRenote(note)) {
 					const reactionsCount = Object.values(this.reactionsBufferingService.mergeReactions(note.renote.reactions, bufferedReactions?.get(note.renote.id)?.deltas ?? {})).reduce((a, b) => a + b, 0);
 					if (reactionsCount === 0) {
 						myReactionsMap.set(note.renote.id, null);

From 2190092de6409c5dbb02a042d98918580171f4c2 Mon Sep 17 00:00:00 2001
From: syuilo <4439005+syuilo@users.noreply.github.com>
Date: Mon, 14 Oct 2024 11:22:02 +0900
Subject: [PATCH 102/121] =?UTF-8?q?perf(frontend):=20=E3=83=8E=E3=83=BC?=
 =?UTF-8?q?=E3=83=88=E3=81=AE=E3=83=AC=E3=83=B3=E3=83=80=E3=83=AA=E3=83=B3?=
 =?UTF-8?q?=E3=82=B0=E3=82=92=E3=82=B9=E3=82=AD=E3=83=83=E3=83=97=E3=81=A7?=
 =?UTF-8?q?=E3=81=8D=E3=82=8B=E3=82=88=E3=81=86=E3=81=AB?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 packages/frontend/src/components/MkNote.vue   | 21 ++++++++-----------
 .../frontend/src/pages/settings/other.vue     |  8 +++++++
 packages/frontend/src/store.ts                |  4 ++++
 3 files changed, 21 insertions(+), 12 deletions(-)

diff --git a/packages/frontend/src/components/MkNote.vue b/packages/frontend/src/components/MkNote.vue
index be93b3c529..828ad2e872 100644
--- a/packages/frontend/src/components/MkNote.vue
+++ b/packages/frontend/src/components/MkNote.vue
@@ -9,7 +9,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 	v-show="!isDeleted"
 	ref="rootEl"
 	v-hotkey="keymap"
-	:class="[$style.root, { [$style.showActionsOnlyHover]: defaultStore.state.showNoteActionsOnlyHover }]"
+	:class="[$style.root, { [$style.showActionsOnlyHover]: defaultStore.state.showNoteActionsOnlyHover, [$style.skipRender]: defaultStore.state.skipNoteRender }]"
 	:tabindex="isDeleted ? '-1' : '0'"
 >
 	<MkNoteSub v-if="appearNote.reply && !renoteCollapsed" :note="appearNote.reply" :class="$style.replyTo"/>
@@ -171,6 +171,9 @@ import { computed, inject, onMounted, ref, shallowRef, Ref, watch, provide } fro
 import * as mfm from 'mfm-js';
 import * as Misskey from 'misskey-js';
 import { isLink } from '@@/js/is-link.js';
+import { shouldCollapsed } from '@@/js/collapsed.js';
+import { host } from '@@/js/config.js';
+import type { MenuItem } from '@/types/menu.js';
 import MkNoteSub from '@/components/MkNoteSub.vue';
 import MkNoteHeader from '@/components/MkNoteHeader.vue';
 import MkNoteSimple from '@/components/MkNoteSimple.vue';
@@ -200,11 +203,8 @@ import { deepClone } from '@/scripts/clone.js';
 import { useTooltip } from '@/scripts/use-tooltip.js';
 import { claimAchievement } from '@/scripts/achievements.js';
 import { getNoteSummary } from '@/scripts/get-note-summary.js';
-import type { MenuItem } from '@/types/menu.js';
 import MkRippleEffect from '@/components/MkRippleEffect.vue';
 import { showMovedDialog } from '@/scripts/show-moved-dialog.js';
-import { shouldCollapsed } from '@@/js/collapsed.js';
-import { host } from '@@/js/config.js';
 import { isEnabledUrlPreview } from '@/instance.js';
 import { type Keymap } from '@/scripts/hotkey.js';
 import { focusPrev, focusNext } from '@/scripts/focus.js';
@@ -619,14 +619,6 @@ function emitUpdReaction(emoji: string, delta: number) {
 	overflow: clip;
 	contain: content;
 
-	// これらの指定はパフォーマンス向上には有効だが、ノートの高さは一定でないため、
-	// 下の方までスクロールすると上のノートの高さがここで決め打ちされたものに変化し、表示しているノートの位置が変わってしまう
-	// ノートがマウントされたときに自身の高さを取得し contain-intrinsic-size を設定しなおせばほぼ解決できそうだが、
-	// 今度はその処理自体がパフォーマンス低下の原因にならないか懸念される。また、被リアクションでも高さは変化するため、やはり多少のズレは生じる
-	// 一度レンダリングされた要素はブラウザがよしなにサイズを覚えておいてくれるような実装になるまで待った方が良さそう(なるのか?)
-	//content-visibility: auto;
-	//contain-intrinsic-size: 0 128px;
-
 	&:focus-visible {
 		outline: none;
 
@@ -687,6 +679,11 @@ function emitUpdReaction(emoji: string, delta: number) {
 	}
 }
 
+.skipRender {
+	content-visibility: auto;
+	contain-intrinsic-size: 0 150px;
+}
+
 .tip {
 	display: flex;
 	align-items: center;
diff --git a/packages/frontend/src/pages/settings/other.vue b/packages/frontend/src/pages/settings/other.vue
index 410a3f53c7..4a52e59d02 100644
--- a/packages/frontend/src/pages/settings/other.vue
+++ b/packages/frontend/src/pages/settings/other.vue
@@ -54,6 +54,9 @@ SPDX-License-Identifier: AGPL-3.0-only
 					<MkSwitch v-model="enableCondensedLine">
 						<template #label>Enable condensed line</template>
 					</MkSwitch>
+					<MkSwitch v-model="skipNoteRender">
+						<template #label>Enable note render skipping</template>
+					</MkSwitch>
 				</div>
 			</MkFolder>
 
@@ -105,9 +108,14 @@ const $i = signinRequired();
 
 const reportError = computed(defaultStore.makeGetterSetter('reportError'));
 const enableCondensedLine = computed(defaultStore.makeGetterSetter('enableCondensedLine'));
+const skipNoteRender = computed(defaultStore.makeGetterSetter('skipNoteRender'));
 const devMode = computed(defaultStore.makeGetterSetter('devMode'));
 const defaultWithReplies = computed(defaultStore.makeGetterSetter('defaultWithReplies'));
 
+watch(skipNoteRender, async () => {
+	await reloadAsk({ reason: i18n.ts.reloadToApplySetting, unison: true });
+});
+
 async function deleteAccount() {
 	{
 		const { canceled } = await os.confirm({
diff --git a/packages/frontend/src/store.ts b/packages/frontend/src/store.ts
index cb52938980..4f641e7513 100644
--- a/packages/frontend/src/store.ts
+++ b/packages/frontend/src/store.ts
@@ -468,6 +468,10 @@ export const defaultStore = markRaw(new Storage('base', {
 		where: 'device',
 		default: 'app' as 'app' | 'appWithShift' | 'native',
 	},
+	skipNoteRender: {
+		where: 'device',
+		default: false,
+	},
 
 	sound_masterVolume: {
 		where: 'device',

From 521d92014db757192b09d62f627f5f1b3ae7c5f5 Mon Sep 17 00:00:00 2001
From: syuilo <4439005+syuilo@users.noreply.github.com>
Date: Mon, 14 Oct 2024 11:22:51 +0900
Subject: [PATCH 103/121] New Crowdin updates (#14753)

* New translations ja-jp.yml (Chinese Traditional)

* New translations ja-jp.yml (Chinese Traditional)

* New translations ja-jp.yml (Chinese Traditional)

* New translations ja-jp.yml (Korean)

* New translations ja-jp.yml (Chinese Simplified)

* New translations ja-jp.yml (Catalan)

* New translations ja-jp.yml (Chinese Simplified)

* New translations ja-jp.yml (Chinese Traditional)

* New translations ja-jp.yml (Korean)

* New translations ja-jp.yml (Chinese Traditional)

* New translations ja-jp.yml (Chinese Simplified)

* New translations ja-jp.yml (Italian)

* New translations ja-jp.yml (Italian)
---
 locales/ca-ES.yml |  2 ++
 locales/it-IT.yml | 32 ++++++++++++++++++++++++++------
 locales/ko-KR.yml |  9 +++++++++
 locales/zh-CN.yml | 11 ++++++++++-
 locales/zh-TW.yml | 38 +++++++++++++++++++++++++++++---------
 5 files changed, 76 insertions(+), 16 deletions(-)

diff --git a/locales/ca-ES.yml b/locales/ca-ES.yml
index 7b668e5ce9..b9f3fecc76 100644
--- a/locales/ca-ES.yml
+++ b/locales/ca-ES.yml
@@ -1286,6 +1286,7 @@ passkeyVerificationFailed: "La verificació a fallat"
 passkeyVerificationSucceededButPasswordlessLoginDisabled: "La verificació de la passkey a estat correcta, però s'ha deshabilitat l'inici de sessió sense contrasenya."
 messageToFollower: "Missatge als meus seguidors"
 target: "Assumpte "
+testCaptchaWarning: "És una característica dissenyada per a la prova de CAPTCHA. <strong>No l'utilitzes en l'entorn real.</strong>"
 _abuseUserReport:
   forward: "Reenviar "
   forwardDescription: "Reenvia l'informe a una altra instància com un compte del sistema anònima."
@@ -1430,6 +1431,7 @@ _serverSettings:
   reactionsBufferingDescription: "Quan s'activa aquesta opció millora bastant el rendiment en recuperar les línies de temps reduint la càrrega de la base. Com a contrapunt, augmentarà  l'ús de memòria de Redís. Desactiva aquesta opció en cas de tenir un servidor amb poca memòria o si tens problemes d'inestabilitat."
   inquiryUrl: "URL de consulta "
   inquiryUrlDescription: "Escriu adreça URL per al formulari de consulta per al mantenidor del servidor o una pàgina web amb el contacte d'informació."
+  thisSettingWillAutomaticallyOffWhenModeratorsInactive: "Si no es detecta activitat per part del moderador durant un període de temps, aquesta opció es desactiva automàticament per evitar el correu brossa."
 _accountMigration:
   moveFrom: "Migrar un altre compte a aquest"
   moveFromSub: "Crear un àlies per un altre compte"
diff --git a/locales/it-IT.yml b/locales/it-IT.yml
index d42fff326c..bcabf1bdb6 100644
--- a/locales/it-IT.yml
+++ b/locales/it-IT.yml
@@ -454,6 +454,7 @@ totpDescription: "Puoi autenticarti inserendo un codice OTP tramite la tua App d
 moderator: "Moderatore"
 moderation: "moderazione"
 moderationNote: "Promemoria di moderazione"
+moderationNoteDescription: "Puoi scrivere promemoria condivisi solo tra moderatori."
 addModerationNote: "Aggiungi promemoria di moderazione"
 moderationLogs: "Cronologia di moderazione"
 nUsersMentioned: "{n} profili ne parlano"
@@ -841,7 +842,7 @@ onlineStatus: "Stato di connessione"
 hideOnlineStatus: "Modalità invisibile"
 hideOnlineStatusDescription: "Attivando questa opzione potresti ridurre l'usabilità di alcune funzioni, come la ricerca."
 online: "Online"
-active: "Attività"
+active: "Attivo"
 offline: "Offline"
 notRecommended: "Sconsigliato"
 botProtection: "Protezione contro i bot"
@@ -1086,6 +1087,7 @@ retryAllQueuesConfirmTitle: "Vuoi ritentare adesso?"
 retryAllQueuesConfirmText: "Potrebbe sovraccaricare il server temporaneamente."
 enableChartsForRemoteUser: "Abilita i grafici per i profili remoti"
 enableChartsForFederatedInstances: "Abilita i grafici per le istanze federate"
+enableStatsForFederatedInstances: "Informazioni statistiche sui server federati"
 showClipButtonInNoteFooter: "Aggiungi il bottone Clip tra le azioni delle Note"
 reactionsDisplaySize: "Grandezza delle reazioni"
 limitWidthOfReaction: "Limita la larghezza delle reazioni e ridimensionale"
@@ -1285,6 +1287,19 @@ unknownWebAuthnKey: "Questa è una passkey sconosciuta."
 passkeyVerificationFailed: "La verifica della passkey non è riuscita."
 passkeyVerificationSucceededButPasswordlessLoginDisabled: "La verifica della passkey è riuscita, ma l'accesso senza password è disabilitato."
 messageToFollower: "Messaggio ai follower"
+target: "Riferimento"
+testCaptchaWarning: "Questa funzione è destinata al test CAPTCHA. <strong>Da non utilizzare in ambiente di produzione.</strong>"
+prohibitedWordsForNameOfUser: "Parole proibite (nome utente)"
+prohibitedWordsForNameOfUserDescription: "Il sistema rifiuta di rinominare un utente, se il nome contiene qualsiasi parola nell'elenco. Sono esenti i profili con privilegi di moderazione."
+yourNameContainsProhibitedWords: "Il nome che hai scelto contiene una o più parole vietate"
+yourNameContainsProhibitedWordsDescription: "Se desideri comunque utilizzare questo nome, contatta l''amministrazione."
+_abuseUserReport:
+  forward: "Inoltra"
+  forwardDescription: "Inoltra il report al server remoto, per mezzo di account di sistema, anonimo."
+  resolve: "Risolvi"
+  accept: "Approva"
+  reject: "Rifiuta"
+  resolveTutorial: "Se moderi una segnalazione legittima, scegli \"Approva\" per risolvere positivamente.\nSe la segnalazione non è legittima, seleziona \"Rifiuta\" per risolvere negativamente."
 _delivery:
   status: "Stato della consegna"
   stop: "Sospensione"
@@ -1312,16 +1327,16 @@ _bubbleGame:
 _announcement:
   forExistingUsers: "Solo ai profili attuali"
   forExistingUsersDescription: "L'annuncio sarà visibile solo ai profili esistenti in questo momento. Se disabilitato, sarà visibile anche ai profili che verranno creati dopo la pubblicazione di questo annuncio."
-  needConfirmationToRead: "Richiede la conferma di lettura"
-  needConfirmationToReadDescription: "Sarà visualizzata una finestra di dialogo che richiede la conferma di lettura. Inoltre, non è soggetto a conferme di lettura massicce."
+  needConfirmationToRead: "Conferma di lettura obbligatoria"
+  needConfirmationToReadDescription: "I profili riceveranno una finestra di dialogo che richiede di accettare obbligatoriamente per procedere. Tale richiesta è esente da  \"conferma tutte\"."
   end: "Archivia l'annuncio"
   tooManyActiveAnnouncementDescription: "L'esperienza delle persone può peggiorare se ci sono troppi annunci attivi. Considera anche l'archiviazione degli annunci conclusi."
   readConfirmTitle: "Segnare come già letto?"
   readConfirmText: "Hai già letto \"{title}˝?"
   shouldNotBeUsedToPresentPermanentInfo: "Ti consigliamo di utilizzare gli annunci per pubblicare informazioni tempestive e limitate nel tempo, anziché informazioni importanti a lungo andare nel tempo, poiché potrebbero risultare difficili da ritrovare e peggiorare la fruibilità del servizio, specialmente alle nuove persone iscritte."
   dialogAnnouncementUxWarn: "Ti consigliamo di usarli con cautela, poiché è molto probabile che avere più di un annuncio in stile \"finestra di dialogo\" peggiori sensibilmente la fruibilità del servizio, specialmente alle nuove persone iscritte."
-  silence: "Silenziare gli annunci"
-  silenceDescription: "Se attivi questa opzione, non riceverai notifiche sugli annunci, evitando di contrassegnarle come già lette."
+  silence: "Annuncio silenzioso"
+  silenceDescription: "Attivando questa opzione, non invierai la notifica, evitando che debba essere contrassegnata come già letta."
 _initialAccountSetting:
   accountCreated: "Il tuo profilo è stato creato!"
   letsStartAccountSetup: "Per iniziare, impostiamo il tuo profilo."
@@ -1422,6 +1437,7 @@ _serverSettings:
   reactionsBufferingDescription: "Attivando questa opzione, puoi migliorare significativamente le prestazioni durante la creazione delle reazioni e ridurre il carico sul database. Tuttavia, aumenterà l'impiego di memoria Redis."
   inquiryUrl: "URL di contatto"
   inquiryUrlDescription: "Specificare l'URL al modulo di contatto, oppure le informazioni con i dati di contatto dell'amministrazione."
+  thisSettingWillAutomaticallyOffWhenModeratorsInactive: "Per prevenire SPAM, questa impostazione verrà disattivata automaticamente, se non si rileva alcuna attività di moderazione durante un certo periodo di tempo."
 _accountMigration:
   moveFrom: "Migra un altro profilo dentro a questo"
   moveFromSub: "Crea un alias verso un altro profilo remoto"
@@ -2187,7 +2203,7 @@ _widgets:
   _userList:
     chooseList: "Seleziona una lista"
   clicker: "Cliccaggio"
-  birthdayFollowings: "Chi nacque oggi"
+  birthdayFollowings: "Compleanni del giorno"
 _cw:
   hide: "Nascondere"
   show: "Continua la lettura..."
@@ -2476,6 +2492,8 @@ _webhookSettings:
     abuseReport: "Quando arriva una segnalazione"
     abuseReportResolved: "Quando una segnalazione è risolta"
     userCreated: "Quando viene creato un profilo"
+    inactiveModeratorsWarning: "Quando un profilo moderatore rimane inattivo per un determinato periodo"
+    inactiveModeratorsInvitationOnlyChanged: "Quando la moderazione è rimasta inattiva per un determinato periodo e il sistema è cambiato in modalità \"solo inviti\""
   deleteConfirm: "Vuoi davvero eliminare il Webhook?"
   testRemarks: "Clicca il bottone a destra dell'interruttore, per provare l'invio di un webhook con dati fittizi."
 _abuseReport:
@@ -2521,6 +2539,8 @@ _moderationLogTypes:
   markSensitiveDriveFile: "File nel Drive segnato come esplicito"
   unmarkSensitiveDriveFile: "File nel Drive segnato come non esplicito"
   resolveAbuseReport: "Segnalazione risolta"
+  forwardAbuseReport: "Segnalazione inoltrata"
+  updateAbuseReportNote: "Ha aggiornato la segnalazione"
   createInvitation: "Genera codice di invito"
   createAd: "Banner creato"
   deleteAd: "Banner eliminato"
diff --git a/locales/ko-KR.yml b/locales/ko-KR.yml
index 973140dca2..414202adab 100644
--- a/locales/ko-KR.yml
+++ b/locales/ko-KR.yml
@@ -1087,6 +1087,7 @@ retryAllQueuesConfirmTitle: "지금 다시 시도하시겠습니까?"
 retryAllQueuesConfirmText: "일시적으로 서버의 부하가 증가할 수 있습니다."
 enableChartsForRemoteUser: "리모트 유저의 차트를 생성"
 enableChartsForFederatedInstances: "리모트 서버의 차트를 생성"
+enableStatsForFederatedInstances: "리모트 서버 정보 받아오기"
 showClipButtonInNoteFooter: "노트 동작에 클립을 추가"
 reactionsDisplaySize: "리액션 표시 크기"
 limitWidthOfReaction: "리액션의 최대 폭을 제한하고 작게 표시하기"
@@ -1287,6 +1288,11 @@ passkeyVerificationFailed: "패스키 검증을 실패했습니다."
 passkeyVerificationSucceededButPasswordlessLoginDisabled: "패스키를 검증했으나, 비밀번호 없이 로그인하기가 꺼져 있습니다."
 messageToFollower: "팔로워에 보낼 메시지"
 target: "대상"
+testCaptchaWarning: "CAPTCHA를 테스트하기 위한 기능입니다. <strong>실제 환경에서는 사용하지 마세요.</strong>"
+prohibitedWordsForNameOfUser: "금지 단어 (사용자 이름)"
+prohibitedWordsForNameOfUserDescription: "이 목록에 포함되는 키워드가 사용자 이름에 있는 경우, 일반 사용자는 이름을 바꿀 수 없습니다. 모더레이터 권한을 가진 사용자는 제한 대상에서 제외됩니다."
+yourNameContainsProhibitedWords: "바꾸려는 이름에 금지된 키워드가 포함되어 있습니다."
+yourNameContainsProhibitedWordsDescription: "이름에 금지된 키워드가 있습니다. 이름을 사용해야 하는 경우, 서버 관리자에 문의하세요."
 _abuseUserReport:
   forward: "전달"
   forwardDescription: "익명 시스템 계정을 사용하여 리모트 서버에 신고 내용을 전달할 수 있습니다."
@@ -1431,6 +1437,7 @@ _serverSettings:
   reactionsBufferingDescription: "활성화 한 경우, 리액션 작성 퍼포먼스가 대폭 향상되어 DB의 부하를 줄일 수 있으나, Redis의 메모리 사용량이 많아집니다."
   inquiryUrl: "문의처 URL"
   inquiryUrlDescription: "서버 운영자에게 보내는 문의 양식의 URL이나 운영자의 연락처 등이 적힌 웹 페이지의 URL을 설정합니다."
+  thisSettingWillAutomaticallyOffWhenModeratorsInactive: "일정 기간동안 모더레이터의 활동이 감지되지 않는 경우, 스팸 방지를 위해 이 설정은 자동으로 꺼집니다."
 _accountMigration:
   moveFrom: "다른 계정에서 이 계정으로 이사"
   moveFromSub: "다른 계정에 대한 별칭을 생성"
@@ -2485,6 +2492,8 @@ _webhookSettings:
     abuseReport: "유저롭"
     abuseReportResolved: "받은 신고를 처리했을 때"
     userCreated: "유저가 생성되었을 때"
+    inactiveModeratorsWarning: "모더레이터가 일정 기간동안 활동하지 않은 경우"
+    inactiveModeratorsInvitationOnlyChanged: "모더레이터가 일정 기간 활동하지 않아 시스템에 의해 초대제로 바뀐 경우"
   deleteConfirm: "Webhook을 삭제할까요?"
   testRemarks: "스위치 오른쪽에 있는 버튼을 클릭하여 더미 데이터를 사용한 테스트용 웹 훅을 보낼 수 있습니다."
 _abuseReport:
diff --git a/locales/zh-CN.yml b/locales/zh-CN.yml
index 8b681efb13..b81018cc1f 100644
--- a/locales/zh-CN.yml
+++ b/locales/zh-CN.yml
@@ -1087,6 +1087,7 @@ retryAllQueuesConfirmTitle: "要再尝试一次吗?"
 retryAllQueuesConfirmText: "可能会使服务器负荷在一定时间内增加"
 enableChartsForRemoteUser: "生成远程用户的图表"
 enableChartsForFederatedInstances: "生成远程服务器的图表"
+enableStatsForFederatedInstances: "获取远程服务器的信息"
 showClipButtonInNoteFooter: "在贴文下方显示便签按钮"
 reactionsDisplaySize: "回应显示大小"
 limitWidthOfReaction: "限制回应的最大宽度,并将其缩小显示"
@@ -1287,6 +1288,11 @@ passkeyVerificationFailed: "验证通行密钥失败。"
 passkeyVerificationSucceededButPasswordlessLoginDisabled: "通行密钥验证成功,但账户未开启无密码登录。"
 messageToFollower: "给关注者的消息"
 target: "对象"
+testCaptchaWarning: "此功能为测试 CAPTCHA 用。<strong>请勿在正式环境中使用。</strong>"
+prohibitedWordsForNameOfUser: "用户名中禁止的词"
+prohibitedWordsForNameOfUserDescription: "更改用户名时,如果用户名中包含此列表里的词汇,用户的改名请求将被拒绝。持有管理员权限的用户不受此限制。"
+yourNameContainsProhibitedWords: "目标用户名包含违禁词"
+yourNameContainsProhibitedWordsDescription: "用户名内含有违禁词。若想使用此用户名,请联系服务器管理员。"
 _abuseUserReport:
   forward: "转发"
   forwardDescription: "目标是匿名系统账户,将把举报转发给远程服务器。"
@@ -1431,6 +1437,7 @@ _serverSettings:
   reactionsBufferingDescription: "开启时可显著提高发送回应时的性能,及减轻数据库负荷。但 Redis 的内存用量会相应增加。"
   inquiryUrl: "联络地址"
   inquiryUrlDescription: "用来指定诸如向服务运营商咨询的论坛地址,或记载了运营商联系方式之类的网页地址。"
+  thisSettingWillAutomaticallyOffWhenModeratorsInactive: "若在一段时间内没有检测到管理活动,为防止垃圾信息,此设定将自动关闭。"
 _accountMigration:
   moveFrom: "从别的账号迁移到此账户"
   moveFromSub: "为另一个账户建立别名"
@@ -2262,7 +2269,7 @@ _profile:
   avatarDecorationMax: "最多可添加 {max} 个挂件"
   followedMessage: "被关注时显示的消息"
   followedMessageDescription: "可以设置被关注时向对方显示的短消息。"
-  followedMessageDescriptionForLockedAccount: "需要批准才能关注的情况下,消息是在被请求被批准后显示。"
+  followedMessageDescriptionForLockedAccount: "需要批准才能关注的情况下,消息是在请求被批准后显示。"
 _exportOrImport:
   allNotes: "所有帖子"
   favoritedNotes: "收藏的帖子"
@@ -2485,6 +2492,8 @@ _webhookSettings:
     abuseReport: "当收到举报时"
     abuseReportResolved: "当举报被处理时"
     userCreated: "当用户被创建时"
+    inactiveModeratorsWarning: "当管理员在一段时间内不活跃时"
+    inactiveModeratorsInvitationOnlyChanged: "当因为管理员在一段时间内不活跃,导致服务器变为邀请制时"
   deleteConfirm: "要删除 webhook 吗?"
   testRemarks: "点击开关右侧的按钮,可以发送使用假数据的测试 Webhook。"
 _abuseReport:
diff --git a/locales/zh-TW.yml b/locales/zh-TW.yml
index 55b504e8fb..de18342bbf 100644
--- a/locales/zh-TW.yml
+++ b/locales/zh-TW.yml
@@ -454,6 +454,7 @@ totpDescription: "以驗證應用程式輸入一次性密碼"
 moderator: "審查員"
 moderation: "審查"
 moderationNote: "管理筆記"
+moderationNoteDescription: "您可以編寫僅在審查員之間共用的註解。"
 addModerationNote: "新增管理筆記"
 moderationLogs: "管理日誌"
 nUsersMentioned: "被 {n} 個人提及"
@@ -519,7 +520,7 @@ menuStyle: "選單風格"
 style: "風格"
 drawer: "側邊欄"
 popup: "彈出式視窗"
-showNoteActionsOnlyHover: "僅在游標停留時顯示貼文的操作選項"
+showNoteActionsOnlyHover: "僅在游標停留時顯示貼文的"
 showReactionsCount: "顯示貼文的反應數目"
 noHistory: "沒有歷史紀錄"
 signinHistory: "登入歷史"
@@ -1018,7 +1019,7 @@ show: "檢視"
 neverShow: "不再顯示"
 remindMeLater: "以後再說"
 didYouLikeMisskey: "您喜歡 Misskey 嗎?"
-pleaseDonate: "Misskey 是由 {host} 使用的免費軟體。請贊助我們,讓開發得以持續!"
+pleaseDonate: "Misskey是由{host}使用的免費軟體。請贊助我們,讓開發的工作能夠持續!"
 correspondingSourceIsAvailable: "對應的原始碼可以在 {anchor} 處找到。"
 roles: "角色"
 role: "角色"
@@ -1086,6 +1087,7 @@ retryAllQueuesConfirmTitle: "要現在重試嗎?"
 retryAllQueuesConfirmText: "伺服器的負荷可能會暫時增加。"
 enableChartsForRemoteUser: "生成遠端使用者的圖表"
 enableChartsForFederatedInstances: "生成遠端伺服器的圖表"
+enableStatsForFederatedInstances: "取得遠端伺服器資訊"
 showClipButtonInNoteFooter: "新增摘錄按鈕至貼文"
 reactionsDisplaySize: "反應的顯示尺寸"
 limitWidthOfReaction: "限制反應的最大寬度,並縮小顯示尺寸。"
@@ -1194,7 +1196,7 @@ showRenotes: "顯示其他人的轉發貼文"
 edited: "已編輯"
 notificationRecieveConfig: "接受通知的設定"
 mutualFollow: "互相追隨"
-followingOrFollower: "追隨中或追隨者"
+followingOrFollower: "追隨中或者追隨者"
 fileAttachedOnly: "只顯示包含附件的貼文"
 showRepliesToOthersInTimeline: "顯示給其他人的回覆"
 hideRepliesToOthersInTimeline: "在時間軸上隱藏給其他人的回覆"
@@ -1265,7 +1267,7 @@ useNativeUIForVideoAudioPlayer: "使用瀏覽器的 UI 播放影片與音訊"
 keepOriginalFilename: "保留原始檔名"
 keepOriginalFilenameDescription: "如果關閉此設置,上傳時檔案名稱會自動替換為隨機字串。"
 noDescription: "沒有說明文字"
-alwaysConfirmFollow: "點擊追隨時總是顯示確認訊息"
+alwaysConfirmFollow: "跟隨時總是確認"
 inquiry: "聯絡我們"
 tryAgain: "請再試一次。"
 confirmWhenRevealingSensitiveMedia: "要顯示敏感媒體時需確認"
@@ -1285,6 +1287,19 @@ unknownWebAuthnKey: "未註冊的金鑰。"
 passkeyVerificationFailed: "驗證金鑰失敗。"
 passkeyVerificationSucceededButPasswordlessLoginDisabled: "雖然驗證金鑰成功,但是無密碼登入的方式是停用的。"
 messageToFollower: "給追隨者的訊息"
+target: "目標 "
+testCaptchaWarning: "此功能用於 CAPTCHA 的測試。<strong>請勿在正式環境中使用。</strong>"
+prohibitedWordsForNameOfUser: "禁止使用的字詞(使用者名稱)"
+prohibitedWordsForNameOfUserDescription: "如果使用者名稱包含此清單中的任何字串,則拒絕重新命名使用者。 具有審查員權限的使用者不受此限制的影響。"
+yourNameContainsProhibitedWords: "您嘗試更改的名稱包含禁止的字串"
+yourNameContainsProhibitedWordsDescription: "名稱中包含禁止使用的字串。 如果您想使用此名稱,請聯絡您的伺服器管理員。"
+_abuseUserReport:
+  forward: "轉發"
+  forwardDescription: "以匿名系統帳戶將檢舉轉發至遠端伺服器。"
+  resolve: "解決"
+  accept: "接受"
+  reject: "拒絕"
+  resolveTutorial: "如果您已回覆正當的檢舉,請選擇「接受」以將案件標記為已解決。\n 如果檢舉的內容不正當,請選擇「拒絕」將案件標記為已解決。"
 _delivery:
   status: "傳送狀態"
   stop: "停止發送"
@@ -1422,6 +1437,7 @@ _serverSettings:
   reactionsBufferingDescription: "啟用時,可以顯著提高建立反應時的效能並減少資料庫的負載。 但是,Redis 記憶體使用量會增加。"
   inquiryUrl: "聯絡表單網址"
   inquiryUrlDescription: "指定伺服器運營者的聯絡表單網址,或包含運營者聯絡資訊網頁的網址。"
+  thisSettingWillAutomaticallyOffWhenModeratorsInactive: "為了防止 spam,如果一段期間內沒有偵測到審查員的活動,此設定將自動關閉。"
 _accountMigration:
   moveFrom: "從其他帳戶遷移到這個帳戶"
   moveFromSub: "為另一個帳戶建立別名"
@@ -1435,7 +1451,7 @@ _accountMigration:
   startMigration: "遷移"
   migrationConfirm: "確定要將這個帳戶遷移至 {account} 嗎?一旦遷移就無法撤銷,也就無法以原來的狀態使用這個帳戶。\n另外,請確認在要遷移到的帳戶已經建立了一個別名。"
   movedAndCannotBeUndone: "帳戶已遷移。\n遷移無法撤消。"
-  postMigrationNote: "在完成遷移的 24 小時後解除此帳戶的追隨。此帳戶的追隨中、追隨者數量變為 0。由於不會解除追隨者,你的追隨者仍然可以繼續檢視這個帳戶發布給追隨者的貼文。"
+  postMigrationNote: "取消追蹤此帳戶將在遷移操作後 24 小時執行。\n 此帳戶有 0 個關注者/關注者。 您的關注者仍然可以看到此帳戶的關注者帖子,因為您不會被取消關注。"
   movedTo: "要遷移到的帳戶:"
 _achievements:
   earnedAt: "獲得日期"
@@ -1555,7 +1571,7 @@ _achievements:
     _markedAsCat:
       title: "我是貓"
       description: "已將帳戶設定為貓"
-      flavor: "還沒有名字。"
+      flavor: "沒有名字。"
     _following1:
       title: "首次追隨"
       description: "首次追隨了"
@@ -1569,7 +1585,7 @@ _achievements:
       title: "一百位朋友"
       description: "追隨超過100人了"
     _following300:
-      title: "朋友過多"
+      title: "朋友太多"
       description: "追隨超過300人了"
     _followers1:
       title: "第一個追隨者"
@@ -1895,7 +1911,7 @@ _channel:
   following: "追隨中"
   usersCount: "有 {n} 人參與"
   notesCount: "有 {n} 篇貼文"
-  nameAndDescription: "名稱與說明"
+  nameAndDescription: "名稱"
   nameOnly: "僅名稱"
   allowRenoteToExternal: "允許在頻道外轉發和引用"
 _menuDisplay:
@@ -2476,6 +2492,8 @@ _webhookSettings:
     abuseReport: "當使用者檢舉時"
     abuseReportResolved: "當處理了使用者的檢舉時"
     userCreated: "使用者被新增時"
+    inactiveModeratorsWarning: "當審查員在一段時間內沒有活動時"
+    inactiveModeratorsInvitationOnlyChanged: "當審查員在一段時間內不活動時,系統會將模式變更為邀請制"
   deleteConfirm: "請問是否要刪除 Webhook?"
   testRemarks: "按下切換開關右側的按鈕,就會將假資料發送至 Webhook。"
 _abuseReport:
@@ -2490,7 +2508,7 @@ _abuseReport:
         mail: "寄送到擁有監察員權限的使用者電子郵件地址(僅在收到檢舉時)"
         webhook: "向指定的 SystemWebhook 發送通知(在收到檢舉和解決檢舉時發送)"
     keywords: "關鍵字"
-    notifiedUser: "被通知的使用者"
+    notifiedUser: "通知的使用者"
     notifiedWebhook: "使用的 Webhook"
     deleteConfirm: "確定要刪除通知對象嗎?"
 _moderationLogTypes:
@@ -2521,6 +2539,8 @@ _moderationLogTypes:
   markSensitiveDriveFile: "標記為敏感檔案"
   unmarkSensitiveDriveFile: "撤銷標記為敏感檔案"
   resolveAbuseReport: "解決檢舉"
+  forwardAbuseReport: "轉發檢舉"
+  updateAbuseReportNote: "更新檢舉的審查備註"
   createInvitation: "建立邀請碼"
   createAd: "建立廣告"
   deleteAd: "刪除廣告"

From 8b7290d6b0aca61d8c57f294a40fd5bd3b19c235 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=8B=E3=81=A3=E3=81=93=E3=81=8B=E3=82=8A?=
 <67428053+kakkokari-gtyih@users.noreply.github.com>
Date: Mon, 14 Oct 2024 11:23:26 +0900
Subject: [PATCH 104/121] =?UTF-8?q?enhance(backend):=20=E5=80=8B=E4=BA=BA?=
 =?UTF-8?q?=E5=AE=9B=E3=81=AE=E3=81=8A=E7=9F=A5=E3=82=89=E3=81=9B=E3=81=AF?=
 =?UTF-8?q?=E3=82=8F=E3=81=8B=E3=81=A3=E3=81=9F=E3=82=92=E6=8A=BC=E3=81=99?=
 =?UTF-8?q?=E3=81=A8=E3=82=A2=E3=83=BC=E3=82=AB=E3=82=A4=E3=83=96=E3=81=99?=
 =?UTF-8?q?=E3=82=8B=E3=82=88=E3=81=86=E3=81=AB=20(#14762)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

* enhance(backend): 個人宛のお知らせはわかったを押すとアーカイブするように

* Update Changelog

* enhance(frontend): アーカイブ済みのものを読み込めるように

* Update Changelog

* fix changelog

* :art:
---
 CHANGELOG.md                                     |  2 ++
 packages/backend/src/core/AnnouncementService.ts |  7 +++++++
 packages/frontend/src/pages/admin-user.vue       | 10 ++++++++++
 3 files changed, 19 insertions(+)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 22b5506f28..9e42d0448e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -9,10 +9,12 @@
 
 ### Client
 - Enhance: l10nの更新
+- Enhance: アーカイブした個人宛のお知らせを表示・編集できるように
 - Fix: メールアドレス不要でCaptchaが有効な場合にアカウント登録完了後自動でのログインに失敗する問題を修正
 
 ### Server
 - Feat: モデレータ権限を持つユーザが全員7日間活動しなかった場合は自動的に招待制へと移行するように ( #13437 )
+- Enhance: 個人宛のお知らせは「わかった」を押すと自動的にアーカイブされるように
 - Fix: `admin/emoji/update`エンドポイントのidのみ指定した時不正なエラーが発生するバグを修正
 - Fix: RBT有効時、リノートのリアクションが反映されない問題を修正
 
diff --git a/packages/backend/src/core/AnnouncementService.ts b/packages/backend/src/core/AnnouncementService.ts
index 40a9db01c0..d4fcf19439 100644
--- a/packages/backend/src/core/AnnouncementService.ts
+++ b/packages/backend/src/core/AnnouncementService.ts
@@ -209,6 +209,13 @@ export class AnnouncementService {
 			return;
 		}
 
+		const announcement = await this.announcementsRepository.findOneBy({ id: announcementId });
+		if (announcement != null && announcement.userId === user.id) {
+			await this.announcementsRepository.update(announcementId, {
+				isActive: false,
+			});
+		}
+
 		if ((await this.getUnreadAnnouncements(user)).length === 0) {
 			this.globalEventService.publishMainStream(user.id, 'readAllAnnouncements');
 		}
diff --git a/packages/frontend/src/pages/admin-user.vue b/packages/frontend/src/pages/admin-user.vue
index d33b116059..948e7a3cce 100644
--- a/packages/frontend/src/pages/admin-user.vue
+++ b/packages/frontend/src/pages/admin-user.vue
@@ -153,6 +153,12 @@ SPDX-License-Identifier: AGPL-3.0-only
 			<div v-else-if="tab === 'announcements'" class="_gaps">
 				<MkButton primary rounded @click="createAnnouncement"><i class="ti ti-plus"></i> {{ i18n.ts.new }}</MkButton>
 
+				<MkSelect v-model="announcementsStatus">
+					<template #label>{{ i18n.ts.filter }}</template>
+					<option value="active">{{ i18n.ts.active }}</option>
+					<option value="archived">{{ i18n.ts.archived }}</option>
+				</MkSelect>
+
 				<MkPagination :pagination="announcementsPagination">
 					<template #default="{ items }">
 						<div class="_gaps_s">
@@ -254,11 +260,15 @@ const filesPagination = {
 		userId: props.userId,
 	})),
 };
+
+const announcementsStatus = ref<'active' | 'archived'>('active');
+
 const announcementsPagination = {
 	endpoint: 'admin/announcements/list' as const,
 	limit: 10,
 	params: computed(() => ({
 		userId: props.userId,
+		status: announcementsStatus.value,
 	})),
 };
 const expandedRoles = ref([]);

From ddca6bdc0171918a0c5b5d8dc61320bd65e4af06 Mon Sep 17 00:00:00 2001
From: "github-actions[bot]" <github-actions[bot]@users.noreply.github.com>
Date: Mon, 14 Oct 2024 02:34:17 +0000
Subject: [PATCH 105/121] Bump version to 2024.10.1-beta.5

---
 package.json                     | 2 +-
 packages/misskey-js/package.json | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/package.json b/package.json
index 4b477aba4b..37a11fb20b 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
 	"name": "misskey",
-	"version": "2024.10.1-beta.4",
+	"version": "2024.10.1-beta.5",
 	"codename": "nasubi",
 	"repository": {
 		"type": "git",
diff --git a/packages/misskey-js/package.json b/packages/misskey-js/package.json
index a59385dc10..590d2367db 100644
--- a/packages/misskey-js/package.json
+++ b/packages/misskey-js/package.json
@@ -1,7 +1,7 @@
 {
 	"type": "module",
 	"name": "misskey-js",
-	"version": "2024.10.1-beta.4",
+	"version": "2024.10.1-beta.5",
 	"description": "Misskey SDK for JavaScript",
 	"license": "MIT",
 	"main": "./built/index.js",

From 64bbce4cf4f6d17c7d3309968d95815f177d9544 Mon Sep 17 00:00:00 2001
From: syuilo <4439005+syuilo@users.noreply.github.com>
Date: Mon, 14 Oct 2024 12:32:59 +0900
Subject: [PATCH 106/121] perf(frontend): improve notification rendering
 performance

---
 packages/frontend/src/components/MkNotification.vue | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/packages/frontend/src/components/MkNotification.vue b/packages/frontend/src/components/MkNotification.vue
index bef425097e..093bdb8b4c 100644
--- a/packages/frontend/src/components/MkNotification.vue
+++ b/packages/frontend/src/components/MkNotification.vue
@@ -220,6 +220,8 @@ function getActualReactedUsersCount(notification: Misskey.entities.Notification)
 	overflow-wrap: break-word;
 	display: flex;
 	contain: content;
+	content-visibility: auto;
+	contain-intrinsic-size: 0 100px;
 
 	--eventFollow: #36aed2;
 	--eventRenote: #36d298;

From c46d6d8edd05b3dd69cf894e29d740d7fe1300ed Mon Sep 17 00:00:00 2001
From: syuilo <4439005+syuilo@users.noreply.github.com>
Date: Mon, 14 Oct 2024 12:34:03 +0900
Subject: [PATCH 107/121] perf(frontend-embed): improve note rendering
 performance

---
 packages/frontend-embed/src/components/EmNote.vue | 14 ++++----------
 1 file changed, 4 insertions(+), 10 deletions(-)

diff --git a/packages/frontend-embed/src/components/EmNote.vue b/packages/frontend-embed/src/components/EmNote.vue
index d4b4827c90..f5b064c293 100644
--- a/packages/frontend-embed/src/components/EmNote.vue
+++ b/packages/frontend-embed/src/components/EmNote.vue
@@ -108,6 +108,8 @@ SPDX-License-Identifier: AGPL-3.0-only
 import { computed, inject, ref, shallowRef } from 'vue';
 import * as mfm from 'mfm-js';
 import * as Misskey from 'misskey-js';
+import { shouldCollapsed } from '@@/js/collapsed.js';
+import { url } from '@@/js/config.js';
 import I18n from '@/components/I18n.vue';
 import EmNoteSub from '@/components/EmNoteSub.vue';
 import EmNoteHeader from '@/components/EmNoteHeader.vue';
@@ -123,8 +125,6 @@ import EmUserName from '@/components/EmUserName.vue';
 import EmTime from '@/components/EmTime.vue';
 import { userPage } from '@/utils.js';
 import { i18n } from '@/i18n.js';
-import { shouldCollapsed } from '@@/js/collapsed.js';
-import { url } from '@@/js/config.js';
 
 function getAppearNote(note: Misskey.entities.Note) {
 	return Misskey.note.isPureRenote(note) ? note.renote : note;
@@ -164,14 +164,8 @@ const isDeleted = ref(false);
 	font-size: 1.05em;
 	overflow: clip;
 	contain: content;
-
-	// これらの指定はパフォーマンス向上には有効だが、ノートの高さは一定でないため、
-	// 下の方までスクロールすると上のノートの高さがここで決め打ちされたものに変化し、表示しているノートの位置が変わってしまう
-	// ノートがマウントされたときに自身の高さを取得し contain-intrinsic-size を設定しなおせばほぼ解決できそうだが、
-	// 今度はその処理自体がパフォーマンス低下の原因にならないか懸念される。また、被リアクションでも高さは変化するため、やはり多少のズレは生じる
-	// 一度レンダリングされた要素はブラウザがよしなにサイズを覚えておいてくれるような実装になるまで待った方が良さそう(なるのか?)
-	//content-visibility: auto;
-  //contain-intrinsic-size: 0 128px;
+	content-visibility: auto;
+  contain-intrinsic-size: 0 150px;
 
 	&:focus-visible {
 		outline: none;

From 3b361a9d0bbc2a6fce6076e379ed08febb447d59 Mon Sep 17 00:00:00 2001
From: syuilo <4439005+syuilo@users.noreply.github.com>
Date: Mon, 14 Oct 2024 12:37:18 +0900
Subject: [PATCH 108/121] perf(frontend): make skipNoteRender on by default

---
 packages/frontend/src/store.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/packages/frontend/src/store.ts b/packages/frontend/src/store.ts
index 4f641e7513..aab67e0b5c 100644
--- a/packages/frontend/src/store.ts
+++ b/packages/frontend/src/store.ts
@@ -470,7 +470,7 @@ export const defaultStore = markRaw(new Storage('base', {
 	},
 	skipNoteRender: {
 		where: 'device',
-		default: false,
+		default: true,
 	},
 
 	sound_masterVolume: {

From 140322b8e2bfce65d39edef0e4cd4f5e93ce1d14 Mon Sep 17 00:00:00 2001
From: syuilo <4439005+syuilo@users.noreply.github.com>
Date: Mon, 14 Oct 2024 12:38:12 +0900
Subject: [PATCH 109/121] Update CHANGELOG.md

---
 CHANGELOG.md | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9e42d0448e..4631615bc7 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -8,8 +8,9 @@
 - Feat: ユーザーの名前に禁止ワードを設定できるように
 
 ### Client
-- Enhance: l10nの更新
+- Enhance: タイムライン表示時のパフォーマンスを向上
 - Enhance: アーカイブした個人宛のお知らせを表示・編集できるように
+- Enhance: l10nの更新
 - Fix: メールアドレス不要でCaptchaが有効な場合にアカウント登録完了後自動でのログインに失敗する問題を修正
 
 ### Server

From 04e74aa28c8cbab840313c2e257896f97fc460fe Mon Sep 17 00:00:00 2001
From: "github-actions[bot]" <github-actions[bot]@users.noreply.github.com>
Date: Mon, 14 Oct 2024 04:19:47 +0000
Subject: [PATCH 110/121] Bump version to 2024.10.1-beta.6

---
 package.json                     | 2 +-
 packages/misskey-js/package.json | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/package.json b/package.json
index 37a11fb20b..59b75fece4 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
 	"name": "misskey",
-	"version": "2024.10.1-beta.5",
+	"version": "2024.10.1-beta.6",
 	"codename": "nasubi",
 	"repository": {
 		"type": "git",
diff --git a/packages/misskey-js/package.json b/packages/misskey-js/package.json
index 590d2367db..0f8433fbb1 100644
--- a/packages/misskey-js/package.json
+++ b/packages/misskey-js/package.json
@@ -1,7 +1,7 @@
 {
 	"type": "module",
 	"name": "misskey-js",
-	"version": "2024.10.1-beta.5",
+	"version": "2024.10.1-beta.6",
 	"description": "Misskey SDK for JavaScript",
 	"license": "MIT",
 	"main": "./built/index.js",

From b0a251d231b18007e0801dbf3517102c6b455320 Mon Sep 17 00:00:00 2001
From: syuilo <4439005+syuilo@users.noreply.github.com>
Date: Mon, 14 Oct 2024 15:35:44 +0900
Subject: [PATCH 111/121] :art:

---
 packages/frontend/src/components/global/MkAd.vue | 4 ----
 1 file changed, 4 deletions(-)

diff --git a/packages/frontend/src/components/global/MkAd.vue b/packages/frontend/src/components/global/MkAd.vue
index 792a087148..0d68d02e35 100644
--- a/packages/frontend/src/components/global/MkAd.vue
+++ b/packages/frontend/src/components/global/MkAd.vue
@@ -136,8 +136,6 @@ function reduceFrequency(): void {
 	}
 
 	&.form_horizontal {
-		padding: 8px;
-
 		> .link,
 		> .link > .img {
 			max-width: min(600px, 100%);
@@ -146,8 +144,6 @@ function reduceFrequency(): void {
 	}
 
 	&.form_horizontalBig {
-		padding: 8px;
-
 		> .link,
 		> .link > .img {
 			max-width: min(600px, 100%);

From 7fd8ef344b33b0a157bc197cbd64069695806936 Mon Sep 17 00:00:00 2001
From: syuilo <4439005+syuilo@users.noreply.github.com>
Date: Mon, 14 Oct 2024 17:43:44 +0900
Subject: [PATCH 112/121] refactor

---
 .../backend/src/core/entities/NoteEntityService.ts   | 12 +-----------
 packages/backend/src/misc/is-renote.ts               | 11 +++++++++++
 2 files changed, 12 insertions(+), 11 deletions(-)

diff --git a/packages/backend/src/core/entities/NoteEntityService.ts b/packages/backend/src/core/entities/NoteEntityService.ts
index e530772dd9..c24c80a5b5 100644
--- a/packages/backend/src/core/entities/NoteEntityService.ts
+++ b/packages/backend/src/core/entities/NoteEntityService.ts
@@ -16,23 +16,13 @@ import { bindThis } from '@/decorators.js';
 import { DebounceLoader } from '@/misc/loader.js';
 import { IdService } from '@/core/IdService.js';
 import { ReactionsBufferingService } from '@/core/ReactionsBufferingService.js';
+import { isPureRenote } from '@/misc/is-renote.js';
 import type { OnModuleInit } from '@nestjs/common';
 import type { CustomEmojiService } from '../CustomEmojiService.js';
 import type { ReactionService } from '../ReactionService.js';
 import type { UserEntityService } from './UserEntityService.js';
 import type { DriveFileEntityService } from './DriveFileEntityService.js';
 
-function isPureRenote(note: MiNote): note is MiNote & { renoteId: MiNote['id']; renote: MiNote } {
-	return (
-		note.renote != null &&
-		note.reply == null &&
-		note.text == null &&
-		note.cw == null &&
-		(note.fileIds == null || note.fileIds.length === 0) &&
-		!note.hasPoll
-	);
-}
-
 function getAppearNoteIds(notes: MiNote[]): Set<string> {
 	const appearNoteIds = new Set<string>();
 	for (const note of notes) {
diff --git a/packages/backend/src/misc/is-renote.ts b/packages/backend/src/misc/is-renote.ts
index 48f821806c..245057c64e 100644
--- a/packages/backend/src/misc/is-renote.ts
+++ b/packages/backend/src/misc/is-renote.ts
@@ -36,6 +36,17 @@ export function isQuote(note: Renote): note is Quote {
 		note.fileIds.length > 0;
 }
 
+export function isPureRenote(note: MiNote): note is MiNote & { renoteId: MiNote['id']; renote: MiNote } {
+	return (
+		note.renote != null &&
+		note.reply == null &&
+		note.text == null &&
+		note.cw == null &&
+		(note.fileIds == null || note.fileIds.length === 0) &&
+		!note.hasPoll
+	);
+}
+
 type PackedRenote =
 	Packed<'Note'> & {
 		renoteId: NonNullable<Packed<'Note'>['renoteId']>

From 77ebabb3dc76d6a422ea576ed60e5e4afe72d637 Mon Sep 17 00:00:00 2001
From: syuilo <4439005+syuilo@users.noreply.github.com>
Date: Mon, 14 Oct 2024 17:51:47 +0900
Subject: [PATCH 113/121] Revert "refactor"

This reverts commit 7fd8ef344b33b0a157bc197cbd64069695806936.
---
 .../backend/src/core/entities/NoteEntityService.ts   | 12 +++++++++++-
 packages/backend/src/misc/is-renote.ts               | 11 -----------
 2 files changed, 11 insertions(+), 12 deletions(-)

diff --git a/packages/backend/src/core/entities/NoteEntityService.ts b/packages/backend/src/core/entities/NoteEntityService.ts
index c24c80a5b5..e530772dd9 100644
--- a/packages/backend/src/core/entities/NoteEntityService.ts
+++ b/packages/backend/src/core/entities/NoteEntityService.ts
@@ -16,13 +16,23 @@ import { bindThis } from '@/decorators.js';
 import { DebounceLoader } from '@/misc/loader.js';
 import { IdService } from '@/core/IdService.js';
 import { ReactionsBufferingService } from '@/core/ReactionsBufferingService.js';
-import { isPureRenote } from '@/misc/is-renote.js';
 import type { OnModuleInit } from '@nestjs/common';
 import type { CustomEmojiService } from '../CustomEmojiService.js';
 import type { ReactionService } from '../ReactionService.js';
 import type { UserEntityService } from './UserEntityService.js';
 import type { DriveFileEntityService } from './DriveFileEntityService.js';
 
+function isPureRenote(note: MiNote): note is MiNote & { renoteId: MiNote['id']; renote: MiNote } {
+	return (
+		note.renote != null &&
+		note.reply == null &&
+		note.text == null &&
+		note.cw == null &&
+		(note.fileIds == null || note.fileIds.length === 0) &&
+		!note.hasPoll
+	);
+}
+
 function getAppearNoteIds(notes: MiNote[]): Set<string> {
 	const appearNoteIds = new Set<string>();
 	for (const note of notes) {
diff --git a/packages/backend/src/misc/is-renote.ts b/packages/backend/src/misc/is-renote.ts
index 245057c64e..48f821806c 100644
--- a/packages/backend/src/misc/is-renote.ts
+++ b/packages/backend/src/misc/is-renote.ts
@@ -36,17 +36,6 @@ export function isQuote(note: Renote): note is Quote {
 		note.fileIds.length > 0;
 }
 
-export function isPureRenote(note: MiNote): note is MiNote & { renoteId: MiNote['id']; renote: MiNote } {
-	return (
-		note.renote != null &&
-		note.reply == null &&
-		note.text == null &&
-		note.cw == null &&
-		(note.fileIds == null || note.fileIds.length === 0) &&
-		!note.hasPoll
-	);
-}
-
 type PackedRenote =
 	Packed<'Note'> & {
 		renoteId: NonNullable<Packed<'Note'>['renoteId']>

From f13c3909a09a73be9952723c431decbb0df67fef Mon Sep 17 00:00:00 2001
From: syuilo <4439005+syuilo@users.noreply.github.com>
Date: Mon, 14 Oct 2024 17:54:27 +0900
Subject: [PATCH 114/121] refactor(backend): remove unnecessary any

---
 packages/backend/src/core/entities/NoteEntityService.ts | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/packages/backend/src/core/entities/NoteEntityService.ts b/packages/backend/src/core/entities/NoteEntityService.ts
index e530772dd9..c6e176d055 100644
--- a/packages/backend/src/core/entities/NoteEntityService.ts
+++ b/packages/backend/src/core/entities/NoteEntityService.ts
@@ -113,7 +113,7 @@ export class NoteEntityService implements OnModuleInit {
 				hide = false;
 			} else {
 				// 指定されているかどうか
-				const specified = packedNote.visibleUserIds!.some((id: any) => meId === id);
+				const specified = packedNote.visibleUserIds!.some(id => meId === id);
 
 				if (specified) {
 					hide = false;
@@ -250,7 +250,7 @@ export class NoteEntityService implements OnModuleInit {
 				return true;
 			} else {
 				// 指定されているかどうか
-				return note.visibleUserIds.some((id: any) => meId === id);
+				return note.visibleUserIds.some(id => meId === id);
 			}
 		}
 

From 5005cc8ae358cf61ae104e39282838d219538f3d Mon Sep 17 00:00:00 2001
From: syuilo <4439005+syuilo@users.noreply.github.com>
Date: Mon, 14 Oct 2024 21:00:20 +0900
Subject: [PATCH 115/121] add note

---
 packages/backend/src/core/entities/NoteEntityService.ts | 1 +
 1 file changed, 1 insertion(+)

diff --git a/packages/backend/src/core/entities/NoteEntityService.ts b/packages/backend/src/core/entities/NoteEntityService.ts
index c6e176d055..3e1f094fce 100644
--- a/packages/backend/src/core/entities/NoteEntityService.ts
+++ b/packages/backend/src/core/entities/NoteEntityService.ts
@@ -22,6 +22,7 @@ import type { ReactionService } from '../ReactionService.js';
 import type { UserEntityService } from './UserEntityService.js';
 import type { DriveFileEntityService } from './DriveFileEntityService.js';
 
+// is-renote.tsとよしなにリンク
 function isPureRenote(note: MiNote): note is MiNote & { renoteId: MiNote['id']; renote: MiNote } {
 	return (
 		note.renote != null &&

From b5de52554834744e4938eee118b43c6cd286ac30 Mon Sep 17 00:00:00 2001
From: syuilo <4439005+syuilo@users.noreply.github.com>
Date: Tue, 15 Oct 2024 10:32:00 +0900
Subject: [PATCH 116/121] add note

---
 packages/backend/src/misc/is-renote.ts | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/packages/backend/src/misc/is-renote.ts b/packages/backend/src/misc/is-renote.ts
index 48f821806c..f4bb329d80 100644
--- a/packages/backend/src/misc/is-renote.ts
+++ b/packages/backend/src/misc/is-renote.ts
@@ -6,6 +6,8 @@
 import type { MiNote } from '@/models/Note.js';
 import type { Packed } from '@/misc/json-schema.js';
 
+// NoteEntityService.isPureRenote とよしなにリンク
+
 type Renote =
 	MiNote & {
 		renoteId: NonNullable<MiNote['renoteId']>

From 825d2186929ea5c819adcafd4cc73743c57b7a14 Mon Sep 17 00:00:00 2001
From: syuilo <4439005+syuilo@users.noreply.github.com>
Date: Tue, 15 Oct 2024 10:39:36 +0900
Subject: [PATCH 117/121] Update CHANGELOG.md

---
 CHANGELOG.md | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4631615bc7..399eca0f75 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,8 +1,8 @@
 ## 2024.10.1
+
 ### Note
-- 悪質なユーザからサーバを守る措置の一環として、モデレータ権限を持つユーザの最終アクティブ日時を確認し、 
-7日間活動していない場合は自動的に招待制へと移行(コントロールパネル -> モデレーション -> "誰でも新規登録できるようにする"をオフに変更)するようになりました。  
-詳細な経緯は https://github.com/misskey-dev/misskey/issues/13437 をご確認ください。
+- スパム対策として、モデレータ権限を持つユーザのアクティビティが7日以上確認できない場合は自動的に招待制へと切り替え(コントロールパネル -> モデレーション -> "誰でも新規登録できるようにする"をオフに変更)るようになりました。 ( #13437 )
+	- 切り替わった際はモデレーターへお知らせとして通知されます。登録をオープンな状態で継続したい場合は、コントロールパネルから再度設定を行ってください。
 
 ### General
 - Feat: ユーザーの名前に禁止ワードを設定できるように
@@ -14,7 +14,7 @@
 - Fix: メールアドレス不要でCaptchaが有効な場合にアカウント登録完了後自動でのログインに失敗する問題を修正
 
 ### Server
-- Feat: モデレータ権限を持つユーザが全員7日間活動しなかった場合は自動的に招待制へと移行するように ( #13437 )
+- Feat: モデレータ権限を持つユーザが全員7日間活動しなかった場合は自動的に招待制へと切り替えるように ( #13437 )
 - Enhance: 個人宛のお知らせは「わかった」を押すと自動的にアーカイブされるように
 - Fix: `admin/emoji/update`エンドポイントのidのみ指定した時不正なエラーが発生するバグを修正
 - Fix: RBT有効時、リノートのリアクションが反映されない問題を修正

From 21a2aa5243c68c070bf73de514ff3884134dd260 Mon Sep 17 00:00:00 2001
From: syuilo <4439005+syuilo@users.noreply.github.com>
Date: Tue, 15 Oct 2024 12:30:40 +0900
Subject: [PATCH 118/121] Update CHANGELOG.md

---
 CHANGELOG.md | 2 --
 1 file changed, 2 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 399eca0f75..504c1bbef6 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -18,8 +18,6 @@
 - Enhance: 個人宛のお知らせは「わかった」を押すと自動的にアーカイブされるように
 - Fix: `admin/emoji/update`エンドポイントのidのみ指定した時不正なエラーが発生するバグを修正
 - Fix: RBT有効時、リノートのリアクションが反映されない問題を修正
-
-### Server
 - Fix: キューのエラーログを簡略化するように  
   (Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/649)
 

From 3cea890eba0b5137adcc4cb0d4fa2b2286914892 Mon Sep 17 00:00:00 2001
From: syuilo <4439005+syuilo@users.noreply.github.com>
Date: Tue, 15 Oct 2024 13:01:06 +0900
Subject: [PATCH 119/121] =?UTF-8?q?fix(frontend):=20blink=E3=82=A2?=
 =?UTF-8?q?=E3=83=8B=E3=83=A1=E3=83=BC=E3=82=B7=E3=83=A7=E3=83=B3=E3=81=8C?=
 =?UTF-8?q?=E5=8B=95=E4=BD=9C=E3=81=97=E3=81=A6=E3=81=84=E3=81=AA=E3=81=84?=
 =?UTF-8?q?=E3=81=AE=E3=82=92=E4=BF=AE=E6=AD=A3?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 packages/frontend/src/components/MkLaunchPad.vue      |  5 ++---
 packages/frontend/src/components/MkMenu.vue           |  9 ++++-----
 packages/frontend/src/style.scss                      |  6 +++++-
 .../frontend/src/ui/_common_/navbar-for-mobile.vue    |  5 ++---
 packages/frontend/src/ui/_common_/navbar.vue          |  6 ++----
 packages/frontend/src/ui/classic.header.vue           |  5 ++---
 packages/frontend/src/ui/classic.sidebar.vue          |  5 ++---
 packages/frontend/src/ui/deck.vue                     |  7 +++----
 packages/frontend/src/ui/universal.vue                | 11 +++++------
 9 files changed, 27 insertions(+), 32 deletions(-)

diff --git a/packages/frontend/src/components/MkLaunchPad.vue b/packages/frontend/src/components/MkLaunchPad.vue
index 2dcba7a50e..32c1a2d172 100644
--- a/packages/frontend/src/components/MkLaunchPad.vue
+++ b/packages/frontend/src/components/MkLaunchPad.vue
@@ -12,13 +12,13 @@ SPDX-License-Identifier: AGPL-3.0-only
 					<i class="icon" :class="item.icon"></i>
 					<div class="text">{{ item.text }}</div>
 					<span v-if="item.indicate && item.indicateValue" class="_indicateCounter indicatorWithValue">{{ item.indicateValue }}</span>
-					<span v-else-if="item.indicate" class="indicator"><i class="_indicatorCircle"></i></span>
+					<span v-else-if="item.indicate" class="indicator _blink"><i class="_indicatorCircle"></i></span>
 				</button>
 				<MkA v-else v-click-anime :to="item.to" class="item" @click.passive="close()">
 					<i class="icon" :class="item.icon"></i>
 					<div class="text">{{ item.text }}</div>
 					<span v-if="item.indicate && item.indicateValue" class="_indicateCounter indicatorWithValue">{{ item.indicateValue }}</span>
-					<span v-else-if="item.indicate" class="indicator"><i class="_indicatorCircle"></i></span>
+					<span v-else-if="item.indicate" class="indicator _blink"><i class="_indicatorCircle"></i></span>
 				</MkA>
 			</template>
 		</div>
@@ -139,7 +139,6 @@ function close() {
 				left: 32px;
 				color: var(--MI_THEME-indicator);
 				font-size: 8px;
-				animation: global-blink 1s infinite;
 
 				@media (max-width: 500px) {
 					top: 16px;
diff --git a/packages/frontend/src/components/MkMenu.vue b/packages/frontend/src/components/MkMenu.vue
index 59f36f8eec..13a65e411f 100644
--- a/packages/frontend/src/components/MkMenu.vue
+++ b/packages/frontend/src/components/MkMenu.vue
@@ -49,7 +49,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 				<MkAvatar v-if="item.avatar" :user="item.avatar" :class="$style.avatar"/>
 				<div :class="$style.item_content">
 					<span :class="$style.item_content_text">{{ item.text }}</span>
-					<span v-if="item.indicate" :class="$style.indicator"><i class="_indicatorCircle"></i></span>
+					<span v-if="item.indicate" :class="$style.indicator" class="_blink"><i class="_indicatorCircle"></i></span>
 				</div>
 			</MkA>
 			<a
@@ -68,7 +68,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 				<i v-if="item.icon" class="ti-fw" :class="[$style.icon, item.icon]"></i>
 				<div :class="$style.item_content">
 					<span :class="$style.item_content_text">{{ item.text }}</span>
-					<span v-if="item.indicate" :class="$style.indicator"><i class="_indicatorCircle"></i></span>
+					<span v-if="item.indicate" :class="$style.indicator" class="_blink"><i class="_indicatorCircle"></i></span>
 				</div>
 			</a>
 			<button
@@ -82,7 +82,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 			>
 				<MkAvatar :user="item.user" :class="$style.avatar"/><MkUserName :user="item.user"/>
 				<div v-if="item.indicate" :class="$style.item_content">
-					<span :class="$style.indicator"><i class="_indicatorCircle"></i></span>
+					<span :class="$style.indicator" class="_blink"><i class="_indicatorCircle"></i></span>
 				</div>
 			</button>
 			<button
@@ -161,7 +161,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 				<MkAvatar v-if="item.avatar" :user="item.avatar" :class="$style.avatar"/>
 				<div :class="$style.item_content">
 					<span :class="$style.item_content_text">{{ item.text }}</span>
-					<span v-if="item.indicate" :class="$style.indicator"><i class="_indicatorCircle"></i></span>
+					<span v-if="item.indicate" :class="$style.indicator" class="_blink"><i class="_indicatorCircle"></i></span>
 				</div>
 			</button>
 		</template>
@@ -639,7 +639,6 @@ onBeforeUnmount(() => {
 	align-items: center;
 	color: var(--MI_THEME-indicator);
 	font-size: 12px;
-	animation: global-blink 1s infinite;
 }
 
 .divider {
diff --git a/packages/frontend/src/style.scss b/packages/frontend/src/style.scss
index 1e6561bdb9..4204c5af59 100644
--- a/packages/frontend/src/style.scss
+++ b/packages/frontend/src/style.scss
@@ -480,7 +480,11 @@ html[data-color-scheme=dark] ._woodenFrame {
 	transform: scale(0.9);
 }
 
-@keyframes global-blink {
+._blink {
+	animation: blink 1s infinite;
+}
+
+@keyframes blink {
 	0% { opacity: 1; transform: scale(1); }
 	30% { opacity: 1; transform: scale(1); }
 	90% { opacity: 0; transform: scale(0.5); }
diff --git a/packages/frontend/src/ui/_common_/navbar-for-mobile.vue b/packages/frontend/src/ui/_common_/navbar-for-mobile.vue
index 9acf7b2ede..44253e93bd 100644
--- a/packages/frontend/src/ui/_common_/navbar-for-mobile.vue
+++ b/packages/frontend/src/ui/_common_/navbar-for-mobile.vue
@@ -19,7 +19,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 			<div v-if="item === '-'" :class="$style.divider"></div>
 			<component :is="navbarItemDef[item].to ? 'MkA' : 'button'" v-else-if="navbarItemDef[item] && (navbarItemDef[item].show !== false)" class="_button" :class="[$style.item, { [$style.active]: navbarItemDef[item].active }]" :activeClass="$style.active" :to="navbarItemDef[item].to" v-on="navbarItemDef[item].action ? { click: navbarItemDef[item].action } : {}">
 				<i class="ti-fw" :class="[$style.itemIcon, navbarItemDef[item].icon]"></i><span :class="$style.itemText">{{ navbarItemDef[item].title }}</span>
-				<span v-if="navbarItemDef[item].indicated" :class="$style.itemIndicator">
+				<span v-if="navbarItemDef[item].indicated" :class="$style.itemIndicator" class="_blink">
 					<span v-if="navbarItemDef[item].indicateValue" class="_indicateCounter" :class="$style.itemIndicateValueIcon">{{ navbarItemDef[item].indicateValue }}</span>
 					<i v-else class="_indicatorCircle"></i>
 				</span>
@@ -31,7 +31,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 		</MkA>
 		<button :class="$style.item" class="_button" @click="more">
 			<i :class="$style.itemIcon" class="ti ti-grid-dots ti-fw"></i><span :class="$style.itemText">{{ i18n.ts.more }}</span>
-			<span v-if="otherMenuItemIndicated" :class="$style.itemIndicator"><i class="_indicatorCircle"></i></span>
+			<span v-if="otherMenuItemIndicated" :class="$style.itemIndicator" class="_blink"><i class="_indicatorCircle"></i></span>
 		</button>
 		<MkA :class="$style.item" :activeClass="$style.active" to="/settings">
 			<i :class="$style.itemIcon" class="ti ti-settings ti-fw"></i><span :class="$style.itemText">{{ i18n.ts.settings }}</span>
@@ -257,7 +257,6 @@ function more() {
 	left: 20px;
 	color: var(--MI_THEME-navIndicator);
 	font-size: 8px;
-	animation: global-blink 1s infinite;
 
 	&:has(.itemIndicateValueIcon) {
 		animation: none;
diff --git a/packages/frontend/src/ui/_common_/navbar.vue b/packages/frontend/src/ui/_common_/navbar.vue
index cbfdaac235..8ae11efa2c 100644
--- a/packages/frontend/src/ui/_common_/navbar.vue
+++ b/packages/frontend/src/ui/_common_/navbar.vue
@@ -29,7 +29,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 					v-on="navbarItemDef[item].action ? { click: navbarItemDef[item].action } : {}"
 				>
 					<i class="ti-fw" :class="[$style.itemIcon, navbarItemDef[item].icon]"></i><span :class="$style.itemText">{{ navbarItemDef[item].title }}</span>
-					<span v-if="navbarItemDef[item].indicated" :class="$style.itemIndicator">
+					<span v-if="navbarItemDef[item].indicated" :class="$style.itemIndicator" class="_blink">
 						<span v-if="navbarItemDef[item].indicateValue" class="_indicateCounter" :class="$style.itemIndicateValueIcon">{{ navbarItemDef[item].indicateValue }}</span>
 						<i v-else class="_indicatorCircle"></i>
 					</span>
@@ -41,7 +41,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 			</MkA>
 			<button class="_button" :class="$style.item" @click="more">
 				<i :class="$style.itemIcon" class="ti ti-grid-dots ti-fw"></i><span :class="$style.itemText">{{ i18n.ts.more }}</span>
-				<span v-if="otherMenuItemIndicated" :class="$style.itemIndicator"><i class="_indicatorCircle"></i></span>
+				<span v-if="otherMenuItemIndicated" :class="$style.itemIndicator" class="_blink"><i class="_indicatorCircle"></i></span>
 			</button>
 			<MkA v-tooltip.noDelay.right="i18n.ts.settings" :class="$style.item" :activeClass="$style.active" to="/settings">
 				<i :class="$style.itemIcon" class="ti ti-settings ti-fw"></i><span :class="$style.itemText">{{ i18n.ts.settings }}</span>
@@ -350,7 +350,6 @@ function more(ev: MouseEvent) {
 		left: 20px;
 		color: var(--MI_THEME-navIndicator);
 		font-size: 8px;
-		animation: global-blink 1s infinite;
 
 		&:has(.itemIndicateValueIcon) {
 			animation: none;
@@ -555,7 +554,6 @@ function more(ev: MouseEvent) {
 		left: 24px;
 		color: var(--MI_THEME-navIndicator);
 		font-size: 8px;
-		animation: global-blink 1s infinite;
 
 		&:has(.itemIndicateValueIcon) {
 			animation: none;
diff --git a/packages/frontend/src/ui/classic.header.vue b/packages/frontend/src/ui/classic.header.vue
index a0a8601887..f4633314ae 100644
--- a/packages/frontend/src/ui/classic.header.vue
+++ b/packages/frontend/src/ui/classic.header.vue
@@ -17,7 +17,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 				<div v-if="item === '-'" class="divider"></div>
 				<component :is="navbarItemDef[item].to ? 'MkA' : 'button'" v-else-if="navbarItemDef[item] && (navbarItemDef[item].show !== false)" v-click-anime v-tooltip="navbarItemDef[item].title" class="item _button" :class="item" activeClass="active" :to="navbarItemDef[item].to" v-on="navbarItemDef[item].action ? { click: navbarItemDef[item].action } : {}">
 					<i class="ti-fw" :class="navbarItemDef[item].icon"></i>
-					<span v-if="navbarItemDef[item].indicated" class="indicator"><i class="_indicatorCircle"></i></span>
+					<span v-if="navbarItemDef[item].indicated" class="indicator _blink"><i class="_indicatorCircle"></i></span>
 				</component>
 			</template>
 			<div class="divider"></div>
@@ -26,7 +26,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 			</MkA>
 			<button v-click-anime class="item _button" @click="more">
 				<i class="ti ti-dots ti-fw"></i>
-				<span v-if="otherNavItemIndicated" class="indicator"><i class="_indicatorCircle"></i></span>
+				<span v-if="otherNavItemIndicated" class="indicator _blink"><i class="_indicatorCircle"></i></span>
 			</button>
 		</div>
 		<div class="right">
@@ -142,7 +142,6 @@ onMounted(() => {
 					left: 0;
 					color: var(--MI_THEME-navIndicator);
 					font-size: 8px;
-					animation: global-blink 1s infinite;
 				}
 
 				&:hover {
diff --git a/packages/frontend/src/ui/classic.sidebar.vue b/packages/frontend/src/ui/classic.sidebar.vue
index 4d1846c34c..5acef0bef8 100644
--- a/packages/frontend/src/ui/classic.sidebar.vue
+++ b/packages/frontend/src/ui/classic.sidebar.vue
@@ -21,7 +21,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 		<div v-if="item === '-'" class="divider"></div>
 		<component :is="navbarItemDef[item].to ? 'MkA' : 'button'" v-else-if="navbarItemDef[item] && (navbarItemDef[item].show !== false)" v-click-anime class="item _button" :class="item" activeClass="active" :to="navbarItemDef[item].to" v-on="navbarItemDef[item].action ? { click: navbarItemDef[item].action } : {}">
 			<i class="ti-fw" :class="navbarItemDef[item].icon"></i><span class="text">{{ navbarItemDef[item].title }}</span>
-			<span v-if="navbarItemDef[item].indicated" class="indicator">
+			<span v-if="navbarItemDef[item].indicated" class="indicator _blink">
 				<span v-if="navbarItemDef[item].indicateValue" class="_indicateCounter itemIndicateValueIcon">{{ navbarItemDef[item].indicateValue }}</span>
 				<i v-else class="_indicatorCircle"></i>
 			</span>
@@ -33,7 +33,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 	</MkA>
 	<button v-click-anime class="item _button" @click="more">
 		<i class="ti ti-dots ti-fw"></i><span class="text">{{ i18n.ts.more }}</span>
-		<span v-if="otherNavItemIndicated" class="indicator"><i class="_indicatorCircle"></i></span>
+		<span v-if="otherNavItemIndicated" class="indicator _blink"><i class="_indicatorCircle"></i></span>
 	</button>
 	<MkA v-click-anime class="item" activeClass="active" to="/settings" :behavior="settingsWindowed ? 'window' : null">
 		<i class="ti ti-settings ti-fw"></i><span class="text">{{ i18n.ts.settings }}</span>
@@ -222,7 +222,6 @@ watch(defaultStore.reactiveState.menuDisplay, () => {
 			left: 0;
 			color: var(--MI_THEME-navIndicator);
 			font-size: 8px;
-			animation: global-blink 1s infinite;
 
 			&:has(.itemIndicateValueIcon) {
 				animation: none;
diff --git a/packages/frontend/src/ui/deck.vue b/packages/frontend/src/ui/deck.vue
index 36ffca8264..a1a76a7e7d 100644
--- a/packages/frontend/src/ui/deck.vue
+++ b/packages/frontend/src/ui/deck.vue
@@ -50,11 +50,11 @@ SPDX-License-Identifier: AGPL-3.0-only
 	</div>
 
 	<div v-if="isMobile" :class="$style.nav">
-		<button :class="$style.navButton" class="_button" @click="drawerMenuShowing = true"><i :class="$style.navButtonIcon" class="ti ti-menu-2"></i><span v-if="menuIndicated" :class="$style.navButtonIndicator"><i class="_indicatorCircle"></i></span></button>
+		<button :class="$style.navButton" class="_button" @click="drawerMenuShowing = true"><i :class="$style.navButtonIcon" class="ti ti-menu-2"></i><span v-if="menuIndicated" :class="$style.navButtonIndicator" class="_blink"><i class="_indicatorCircle"></i></span></button>
 		<button :class="$style.navButton" class="_button" @click="mainRouter.push('/')"><i :class="$style.navButtonIcon" class="ti ti-home"></i></button>
 		<button :class="$style.navButton" class="_button" @click="mainRouter.push('/my/notifications')">
 			<i :class="$style.navButtonIcon" class="ti ti-bell"></i>
-			<span v-if="$i?.hasUnreadNotification" :class="$style.navButtonIndicator">
+			<span v-if="$i?.hasUnreadNotification" :class="$style.navButtonIndicator" class="_blink">
 				<span class="_indicateCounter" :class="$style.itemIndicateValueIcon">{{ $i.unreadNotificationsCount > 99 ? '99+' : $i.unreadNotificationsCount }}</span>
 			</span>
 		</button>
@@ -97,6 +97,7 @@ import { v4 as uuid } from 'uuid';
 import XCommon from './_common_/common.vue';
 import { deckStore, columnTypes, addColumn as addColumnToStore, loadDeck, getProfiles, deleteProfile as deleteProfile_ } from './deck/deck-store.js';
 import type { ColumnType } from './deck/deck-store.js';
+import type { MenuItem } from '@/types/menu.js';
 import XSidebar from '@/ui/_common_/navbar.vue';
 import XDrawerMenu from '@/ui/_common_/navbar-for-mobile.vue';
 import MkButton from '@/components/MkButton.vue';
@@ -118,7 +119,6 @@ import XMentionsColumn from '@/ui/deck/mentions-column.vue';
 import XDirectColumn from '@/ui/deck/direct-column.vue';
 import XRoleTimelineColumn from '@/ui/deck/role-timeline-column.vue';
 import { mainRouter } from '@/router/main.js';
-import type { MenuItem } from '@/types/menu.js';
 const XStatusBars = defineAsyncComponent(() => import('@/ui/_common_/statusbars.vue'));
 const XAnnouncements = defineAsyncComponent(() => import('@/ui/_common_/announcements.vue'));
 
@@ -479,7 +479,6 @@ body {
 	left: 0;
 	color: var(--MI_THEME-indicator);
 	font-size: 16px;
-	animation: global-blink 1s infinite;
 
 	&:has(.itemIndicateValueIcon) {
 		animation: none;
diff --git a/packages/frontend/src/ui/universal.vue b/packages/frontend/src/ui/universal.vue
index 9fc8bd102d..d739c2e1cd 100644
--- a/packages/frontend/src/ui/universal.vue
+++ b/packages/frontend/src/ui/universal.vue
@@ -25,11 +25,11 @@ SPDX-License-Identifier: AGPL-3.0-only
 	<button v-if="(!isDesktop || pageMetadata?.needWideArea) && !isMobile" :class="$style.widgetButton" class="_button" @click="widgetsShowing = true"><i class="ti ti-apps"></i></button>
 
 	<div v-if="isMobile" ref="navFooter" :class="$style.nav">
-		<button :class="$style.navButton" class="_button" @click="drawerMenuShowing = true"><i :class="$style.navButtonIcon" class="ti ti-menu-2"></i><span v-if="menuIndicated" :class="$style.navButtonIndicator"><i class="_indicatorCircle"></i></span></button>
+		<button :class="$style.navButton" class="_button" @click="drawerMenuShowing = true"><i :class="$style.navButtonIcon" class="ti ti-menu-2"></i><span v-if="menuIndicated" :class="$style.navButtonIndicator" class="_blink"><i class="_indicatorCircle"></i></span></button>
 		<button :class="$style.navButton" class="_button" @click="isRoot ? top() : mainRouter.push('/')"><i :class="$style.navButtonIcon" class="ti ti-home"></i></button>
 		<button :class="$style.navButton" class="_button" @click="mainRouter.push('/my/notifications')">
 			<i :class="$style.navButtonIcon" class="ti ti-bell"></i>
-			<span v-if="$i?.hasUnreadNotification" :class="$style.navButtonIndicator">
+			<span v-if="$i?.hasUnreadNotification" :class="$style.navButtonIndicator" class="_blink">
 				<span class="_indicateCounter" :class="$style.itemIndicateValueIcon">{{ $i.unreadNotificationsCount > 99 ? '99+' : $i.unreadNotificationsCount }}</span>
 			</span>
 		</button>
@@ -96,9 +96,11 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <script lang="ts" setup>
 import { defineAsyncComponent, provide, onMounted, computed, ref, watch, shallowRef, Ref } from 'vue';
+import { instanceName } from '@@/js/config.js';
+import { CURRENT_STICKY_BOTTOM } from '@@/js/const.js';
+import { isLink } from '@@/js/is-link.js';
 import XCommon from './_common_/common.vue';
 import type MkStickyContainer from '@/components/global/MkStickyContainer.vue';
-import { instanceName } from '@@/js/config.js';
 import XDrawerMenu from '@/ui/_common_/navbar-for-mobile.vue';
 import * as os from '@/os.js';
 import { defaultStore } from '@/store.js';
@@ -108,10 +110,8 @@ import { $i } from '@/account.js';
 import { PageMetadata, provideMetadataReceiver, provideReactiveMetadata } from '@/scripts/page-metadata.js';
 import { deviceKind } from '@/scripts/device-kind.js';
 import { miLocalStorage } from '@/local-storage.js';
-import { CURRENT_STICKY_BOTTOM } from '@@/js/const.js';
 import { useScrollPositionManager } from '@/nirax.js';
 import { mainRouter } from '@/router/main.js';
-import { isLink } from '@@/js/is-link.js';
 
 const XWidgets = defineAsyncComponent(() => import('./universal.widgets.vue'));
 const XSidebar = defineAsyncComponent(() => import('@/ui/_common_/navbar.vue'));
@@ -451,7 +451,6 @@ $widgets-hide-threshold: 1090px;
 	left: 0;
 	color: var(--MI_THEME-indicator);
 	font-size: 16px;
-	animation: global-blink 1s infinite;
 
 	&:has(.itemIndicateValueIcon) {
 		animation: none;

From b990ae6b230840cb7125a7c8d1eafdd7c959bc91 Mon Sep 17 00:00:00 2001
From: zyoshoka <107108195+zyoshoka@users.noreply.github.com>
Date: Tue, 15 Oct 2024 13:37:00 +0900
Subject: [PATCH 120/121] test(backend): add federation test (#14582)

* test(backend): add federation test

* fix(ci): install pnpm

* fix(ci): cd

* fix(ci): build entire project

* fix(ci): skip frontend build

* fix(ci): pull submodule when checkout

* chore: show log for debugging

* Revert "chore: show log for debugging"

This reverts commit a930964b8d6ba550c23bce1e7fb45d92eab49ef9.

* fix(ci): build entire project

* chore: omit unused globals

* refactor: use strictEqual and simplify some asserts

* test: follow requests

* refactor: add resolveRemoteNote function

* refactor: refine resolveRemoteUser function

* refactor: cache admin credentials

* refactor: simplify assertion with excluded fields

* refactor: use assert

* test: note

* chore: labeler detect federation

* test: blocking

* test: move

* fix: use appropriate TLD

* chore: shorter purge interval

* fix(ci): change TLD

* refactor: delete trivial comment

* test(user): isCat

* chore: use jest

* chore: omit logs

* chore: add memo

* fix(ci): omit unnecessary build

* test: pinning Note

* fix: build daemon in container

* style: indent

* test(streaming): timeline

* chore: rename

* fix: delete role after test

* refactor: resolve users by uri

* fix: delete antenna after test

* test: api timeline

* test: Note deletion

* refactor: sleep function

* test: notification

* style: indent

* refactor: type-safe host

* docs: update description

* refactor: resolve function params

* fix(block): wrong test name

* fix: invalid type

* fix: longer timeout for fire testing

* test(timeline): hashtag

* test(note): vote delivery

* fix: wrong description

* fix: hashtag channel param type

* refactor: wrap basic cases

* test(timeline): add homeTimeline tests

* fix(timeline): correct wrong case and description

* test(notification): add tests for Note

* refactor(user): wrap profile consistency with describe

* chore(note): add issue link

* test(timeline): add test

* test(user): suspension

* test: emoji

* refactor: fetch admin first

* perf: faster tests

* test(drive): sensitive flag

* test(emoji): add tests

* chore: ignore .config/docker.env

* chore: hard-coded tester IP address

* test(emoji): custom emoji are surrounded by zero width space

* refactor: client and username as property

* test(notification): mute

* fix(notification): correct description

* test(block): mention

* refactor(emoji): addCustomEmoji function

* fix: typo

* test(note): add reaction tests

* test(timeline): Note deletion

* fix: unnecessary ts-expect-error

* refactor: unnecessary fetch mocking

* chore: add TODO comments

* test(user): deletion

* chore: enable --frozen-lockfile

* fix(ci): copying configs

* docs: update CONTRIBUTING.md

* docs: fix typo

* chore: set default sleep duration

* fix(notification): omit flaky tests

* fix(notification): correct type

* test(notification): add api endpoint tests

* chore: remove redundant mute test

* refactor: use param client

* fix: start timer after trigger

* refactor: remove unnecessary any

* chore: shorter timeout for checking if fired

* fix(block): remove outdated comment

* refactor: shorten remote user variable name

* refactor(block): use existing function

* refactor: file upload

* docs: update description

* test(user): ffVisibility

* fix: `/api/signin` -> `/api/signin-flow`

* test: abuse report

* refactor: use existing type

* refactor: extract duplicate configs to template file

* fix: typo

* fix: avoid conflict

* refactor: change container dependency

* perf: start misskey parallelly

* fix: remove dependency

* chore(backend): add typecheck

* test: add check for #14728

* chore: enable eslint check

* perf: don't start linked services when test

* test(note): remote note deletion for moderation

* chore: define config template

* chore: write setup script

* refactor: omit unnecessary conditional

* refactor: clarify scope

* refactor: omit type assertion

* refactor: omit logs

* style

* refactor: redundant promise

* refactor: unnecessary imports

* refactor: use readable error code

* refactor: cache set in signin function

* refactor: optimize import
---
 .github/labeler.yml                           |   2 +-
 .github/workflows/test-federation.yml         |  59 ++
 .gitignore                                    |   2 +-
 CONTRIBUTING.md                               |  46 +-
 packages/backend/eslint.config.js             |   2 +-
 packages/backend/jest.config.fed.cjs          |  13 +
 packages/backend/package.json                 |   6 +-
 .../test-federation/.config/example.conf      |  70 +++
 .../.config/example.default.yml               |  25 +
 .../.config/example.docker.env                |   5 +
 packages/backend/test-federation/.gitignore   |   6 +
 packages/backend/test-federation/README.md    |  24 +
 .../backend/test-federation/compose.a.yml     |  64 ++
 .../backend/test-federation/compose.b.yml     |  64 ++
 .../test-federation/compose.override.yaml     | 117 ++++
 .../backend/test-federation/compose.tpl.yml   | 101 ++++
 packages/backend/test-federation/compose.yml  | 133 +++++
 packages/backend/test-federation/daemon.ts    |  38 ++
 .../backend/test-federation/eslint.config.js  |  21 +
 packages/backend/test-federation/setup.sh     |  35 ++
 .../test-federation/test/abuse-report.test.ts |  52 ++
 .../test-federation/test/block.test.ts        | 224 +++++++
 .../test-federation/test/drive.test.ts        | 175 ++++++
 .../test-federation/test/emoji.test.ts        |  97 +++
 .../backend/test-federation/test/move.test.ts |  52 ++
 .../backend/test-federation/test/note.test.ts | 317 ++++++++++
 .../test-federation/test/notification.test.ts | 107 ++++
 .../test-federation/test/timeline.test.ts     | 328 ++++++++++
 .../backend/test-federation/test/user.test.ts | 560 ++++++++++++++++++
 .../backend/test-federation/test/utils.ts     | 309 ++++++++++
 .../backend/test-federation/tsconfig.json     | 114 ++++
 packages/shared/eslint.config.js              |   7 +
 32 files changed, 3154 insertions(+), 21 deletions(-)
 create mode 100644 .github/workflows/test-federation.yml
 create mode 100644 packages/backend/jest.config.fed.cjs
 create mode 100644 packages/backend/test-federation/.config/example.conf
 create mode 100644 packages/backend/test-federation/.config/example.default.yml
 create mode 100644 packages/backend/test-federation/.config/example.docker.env
 create mode 100644 packages/backend/test-federation/.gitignore
 create mode 100644 packages/backend/test-federation/README.md
 create mode 100644 packages/backend/test-federation/compose.a.yml
 create mode 100644 packages/backend/test-federation/compose.b.yml
 create mode 100644 packages/backend/test-federation/compose.override.yaml
 create mode 100644 packages/backend/test-federation/compose.tpl.yml
 create mode 100644 packages/backend/test-federation/compose.yml
 create mode 100644 packages/backend/test-federation/daemon.ts
 create mode 100644 packages/backend/test-federation/eslint.config.js
 create mode 100644 packages/backend/test-federation/setup.sh
 create mode 100644 packages/backend/test-federation/test/abuse-report.test.ts
 create mode 100644 packages/backend/test-federation/test/block.test.ts
 create mode 100644 packages/backend/test-federation/test/drive.test.ts
 create mode 100644 packages/backend/test-federation/test/emoji.test.ts
 create mode 100644 packages/backend/test-federation/test/move.test.ts
 create mode 100644 packages/backend/test-federation/test/note.test.ts
 create mode 100644 packages/backend/test-federation/test/notification.test.ts
 create mode 100644 packages/backend/test-federation/test/timeline.test.ts
 create mode 100644 packages/backend/test-federation/test/user.test.ts
 create mode 100644 packages/backend/test-federation/test/utils.ts
 create mode 100644 packages/backend/test-federation/tsconfig.json

diff --git a/.github/labeler.yml b/.github/labeler.yml
index a77f73706b..b64d726d65 100644
--- a/.github/labeler.yml
+++ b/.github/labeler.yml
@@ -6,7 +6,7 @@
 'packages/backend:test':
 - any:
   - changed-files:
-    - any-glob-to-any-file: ['packages/backend/test/**/*']
+    - any-glob-to-any-file: ['packages/backend/test/**/*', 'packages/backend/test-federation/**/*']
 
 'packages/frontend':
 - any:
diff --git a/.github/workflows/test-federation.yml b/.github/workflows/test-federation.yml
new file mode 100644
index 0000000000..183ddb6f34
--- /dev/null
+++ b/.github/workflows/test-federation.yml
@@ -0,0 +1,59 @@
+name: Test (federation)
+
+on:
+  push:
+    branches:
+      - master
+      - develop
+    paths:
+      - packages/backend/**
+      - packages/misskey-js/**
+      - .github/workflows/test-federation.yml
+  pull_request:
+    paths:
+      - packages/backend/**
+      - packages/misskey-js/**
+      - .github/workflows/test-federation.yml
+
+jobs:
+  test:
+    runs-on: ubuntu-latest
+    strategy:
+      matrix:
+        node-version: [20.16.0]
+    steps:
+      - uses: actions/checkout@v4
+        with:
+          submodules: true
+      - name: Install pnpm
+        uses: pnpm/action-setup@v4
+      - name: Install FFmpeg
+        uses: FedericoCarboni/setup-ffmpeg@v3
+      - name: Use Node.js ${{ matrix.node-version }}
+        uses: actions/setup-node@v4.0.3
+        with:
+          node-version: ${{ matrix.node-version }}
+          cache: 'pnpm'
+      - name: Build Misskey
+        run: |
+          corepack enable && corepack prepare
+          pnpm i --frozen-lockfile
+          pnpm build
+      - name: Setup
+        run: |
+          cd packages/backend/test-federation
+          bash ./setup.sh
+          sudo chmod 644 ./certificates/*.test.key
+      - name: Start servers
+        # https://github.com/docker/compose/issues/1294#issuecomment-374847206
+        run: |
+          cd packages/backend/test-federation
+          docker compose up -d --scale tester=0
+      - name: Test
+        run: |
+          cd packages/backend/test-federation
+          docker compose run --no-deps tester
+      - name: Stop servers
+        run: |
+          cd packages/backend/test-federation
+          docker compose down
diff --git a/.gitignore b/.gitignore
index b270d5cb3a..5b8a798ba6 100644
--- a/.gitignore
+++ b/.gitignore
@@ -37,7 +37,7 @@ coverage
 !/.config/docker_example.env
 !/.config/cypress-devcontainer.yml
 docker-compose.yml
-compose.yml
+./compose.yml
 .devcontainer/compose.yml
 !/.devcontainer/compose.yml
 
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 3a4dc7b918..fc72cf42ea 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -181,31 +181,45 @@ MK_DEV_PREFER=backend pnpm dev
 - HMR may not work in some environments such as Windows.
 
 ## Testing
-- Test codes are located in [`/packages/backend/test`](/packages/backend/test).
-
-### Run test
-Create a config file.
+You can run non-backend tests by executing following commands:
+```sh
+pnpm --filter frontend test
+pnpm --filter misskey-js test
 ```
+
+Backend tests require manual preparation of servers. See the next section for more on this.
+
+### Backend
+There are three types of test codes for the backend:
+- Unit tests: [`/packages/backend/test/unit`](/packages/backend/test/unit)
+- Single-server E2E tests: [`/packages/backend/test/e2e`](/packages/backend/test/e2e)
+- Multiple-server E2E tests: [`/packages/backend/test-federation`](/packages/backend/test-federation)
+
+#### Running Unit Tests or Single-server E2E Tests
+1. Create a config file:
+```sh
 cp .github/misskey/test.yml .config/
 ```
-Prepare DB/Redis for testing.
-```
+
+2. Start DB and Redis servers for testing:
+```sh
 docker compose -f packages/backend/test/compose.yml up
 ```
-Alternatively, prepare an empty (data can be erased) DB and edit `.config/test.yml`.
+Instead, you can prepare an empty (data can be erased) DB and edit `.config/test.yml` appropriately.
 
-Run all test.
+3. Run all tests:
+```sh
+pnpm --filter backend test     # unit tests
+pnpm --filter backend test:e2e # single-server E2E tests
 ```
-pnpm test
+If you want to run a specific test, run as a following command:
+```sh
+pnpm --filter backend test -- packages/backend/test/unit/activitypub.ts
+pnpm --filter backend test:e2e -- packages/backend/test/e2e/nodeinfo.ts
 ```
 
-#### Run specify test
-```
-pnpm jest -- foo.ts
-```
-
-### e2e tests
-TODO
+#### Running Multiple-server E2E Tests
+See [`/packages/backend/test-federation/README.md`](/packages/backend/test-federation/README.md).
 
 ## Environment Variable
 
diff --git a/packages/backend/eslint.config.js b/packages/backend/eslint.config.js
index 4fd9f0cd51..ae7b2baf49 100644
--- a/packages/backend/eslint.config.js
+++ b/packages/backend/eslint.config.js
@@ -11,7 +11,7 @@ export default [
 		languageOptions: {
 			parserOptions: {
 				parser: tsParser,
-				project: ['./tsconfig.json', './test/tsconfig.json'],
+				project: ['./tsconfig.json', './test/tsconfig.json', './test-federation/tsconfig.json'],
 				sourceType: 'module',
 				tsconfigRootDir: import.meta.dirname,
 			},
diff --git a/packages/backend/jest.config.fed.cjs b/packages/backend/jest.config.fed.cjs
new file mode 100644
index 0000000000..fae187bc23
--- /dev/null
+++ b/packages/backend/jest.config.fed.cjs
@@ -0,0 +1,13 @@
+/*
+ * For a detailed explanation regarding each configuration property and type check, visit:
+ * https://jestjs.io/docs/en/configuration.html
+ */
+
+const base = require('./jest.config.cjs');
+
+module.exports = {
+	...base,
+	testMatch: [
+		'<rootDir>/test-federation/test/**/*.test.ts',
+	],
+};
diff --git a/packages/backend/package.json b/packages/backend/package.json
index c6e31797f8..0dd738a1e6 100644
--- a/packages/backend/package.json
+++ b/packages/backend/package.json
@@ -19,16 +19,18 @@
 		"watch": "node ./scripts/watch.mjs",
 		"restart": "pnpm build && pnpm start",
 		"dev": "node ./scripts/dev.mjs",
-		"typecheck": "tsc --noEmit && tsc -p test --noEmit",
-		"eslint": "eslint --quiet \"src/**/*.ts\"",
+		"typecheck": "tsc --noEmit && tsc -p test --noEmit && tsc -p test-federation --noEmit",
+		"eslint": "eslint --quiet \"{src,test-federation}/**/*.ts\"",
 		"lint": "pnpm typecheck && pnpm eslint",
 		"jest": "cross-env NODE_ENV=test node --experimental-vm-modules --experimental-import-meta-resolve node_modules/jest/bin/jest.js --forceExit --config jest.config.unit.cjs",
 		"jest:e2e": "cross-env NODE_ENV=test node --experimental-vm-modules --experimental-import-meta-resolve node_modules/jest/bin/jest.js --forceExit --config jest.config.e2e.cjs",
+		"jest:fed": "node --experimental-vm-modules --experimental-import-meta-resolve node_modules/jest/bin/jest.js --forceExit --config jest.config.fed.cjs",
 		"jest-and-coverage": "cross-env NODE_ENV=test node --experimental-vm-modules --experimental-import-meta-resolve node_modules/jest/bin/jest.js --coverage --forceExit --config jest.config.unit.cjs",
 		"jest-and-coverage:e2e": "cross-env NODE_ENV=test node --experimental-vm-modules --experimental-import-meta-resolve node_modules/jest/bin/jest.js --coverage --forceExit --config jest.config.e2e.cjs",
 		"jest-clear": "cross-env NODE_ENV=test node --experimental-vm-modules --experimental-import-meta-resolve node_modules/jest/bin/jest.js --clearCache",
 		"test": "pnpm jest",
 		"test:e2e": "pnpm build && pnpm build:test && pnpm jest:e2e",
+		"test:fed": "pnpm jest:fed",
 		"test-and-coverage": "pnpm jest-and-coverage",
 		"test-and-coverage:e2e": "pnpm build && pnpm build:test && pnpm jest-and-coverage:e2e",
 		"generate-api-json": "node ./scripts/generate_api_json.js"
diff --git a/packages/backend/test-federation/.config/example.conf b/packages/backend/test-federation/.config/example.conf
new file mode 100644
index 0000000000..83d04eb39d
--- /dev/null
+++ b/packages/backend/test-federation/.config/example.conf
@@ -0,0 +1,70 @@
+# based on https://github.com/misskey-dev/misskey-hub/blob/7071f63a1c80ee35c71f0fd8a6d8722c118c7574/src/docs/admin/nginx.md
+
+# For WebSocket
+map $http_upgrade $connection_upgrade {
+	default upgrade;
+	'' close;
+}
+
+proxy_cache_path /tmp/nginx_cache levels=1:2 keys_zone=cache1:16m max_size=1g inactive=720m use_temp_path=off;
+
+server {
+	listen 80;
+	listen [::]:80;
+	server_name ${HOST};
+
+	# For SSL domain validation
+	root /var/www/html;
+	location /.well-known/acme-challenge/ { allow all; }
+	location /.well-known/pki-validation/ { allow all; }
+	location / { return 301 https://$server_name$request_uri; }
+}
+
+server {
+	listen 443 ssl;
+	listen [::]:443 ssl;
+	http2 on;
+	server_name ${HOST};
+
+	ssl_session_timeout 1d;
+	ssl_session_cache shared:ssl_session_cache:10m;
+	ssl_session_tickets off;
+
+	ssl_trusted_certificate /etc/nginx/certificates/rootCA.crt;
+	ssl_certificate /etc/nginx/certificates/$server_name.crt;
+	ssl_certificate_key /etc/nginx/certificates/$server_name.key;
+
+	# SSL protocol settings
+	ssl_protocols TLSv1.2 TLSv1.3;
+	ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
+	ssl_prefer_server_ciphers off;
+	ssl_stapling on;
+	ssl_stapling_verify on;
+
+	# Change to your upload limit
+	client_max_body_size 80m;
+
+	# Proxy to Node
+	location / {
+		proxy_pass http://misskey.${HOST}:3000;
+		proxy_set_header Host $host;
+		proxy_http_version 1.1;
+		proxy_redirect off;
+
+		# If it's behind another reverse proxy or CDN, remove the following.
+		proxy_set_header X-Real-IP $remote_addr;
+		proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+		proxy_set_header X-Forwarded-Proto https;
+
+		# For WebSocket
+		proxy_set_header Upgrade $http_upgrade;
+		proxy_set_header Connection $connection_upgrade;
+
+		# Cache settings
+		proxy_cache cache1;
+		proxy_cache_lock on;
+		proxy_cache_use_stale updating;
+		proxy_force_ranges on;
+		add_header X-Cache $upstream_cache_status;
+	}
+}
diff --git a/packages/backend/test-federation/.config/example.default.yml b/packages/backend/test-federation/.config/example.default.yml
new file mode 100644
index 0000000000..ff1760a5a6
--- /dev/null
+++ b/packages/backend/test-federation/.config/example.default.yml
@@ -0,0 +1,25 @@
+url: https://${HOST}/
+port: 3000
+db:
+  host: db.${HOST}
+  port: 5432
+  db: misskey
+  user: postgres
+  pass: postgres
+dbReplications: false
+redis:
+  host: redis.test
+  port: 6379
+id: 'aidx'
+proxyBypassHosts:
+  - api.deepl.com
+  - api-free.deepl.com
+  - www.recaptcha.net
+  - hcaptcha.com
+  - challenges.cloudflare.com
+proxyRemoteFiles: true
+signToActivityPubGet: true
+allowedPrivateNetworks: [
+  '127.0.0.1/32',
+  '172.20.0.0/16'
+]
diff --git a/packages/backend/test-federation/.config/example.docker.env b/packages/backend/test-federation/.config/example.docker.env
new file mode 100644
index 0000000000..a8af7cce49
--- /dev/null
+++ b/packages/backend/test-federation/.config/example.docker.env
@@ -0,0 +1,5 @@
+NODE_EXTRA_CA_CERTS=/usr/local/share/ca-certificates/rootCA.crt
+POSTGRES_DB=misskey
+POSTGRES_USER=postgres
+POSTGRES_PASSWORD=postgres
+MK_VERBOSE=true
diff --git a/packages/backend/test-federation/.gitignore b/packages/backend/test-federation/.gitignore
new file mode 100644
index 0000000000..e00f952cb5
--- /dev/null
+++ b/packages/backend/test-federation/.gitignore
@@ -0,0 +1,6 @@
+certificates
+volumes
+.env
+docker.env
+*.test.conf
+*.test.default.yml
diff --git a/packages/backend/test-federation/README.md b/packages/backend/test-federation/README.md
new file mode 100644
index 0000000000..967d51f085
--- /dev/null
+++ b/packages/backend/test-federation/README.md
@@ -0,0 +1,24 @@
+## test-federation
+Test federation between two Misskey servers: `a.test` and `b.test`.
+
+Before testing, you need to build the entire project, and change working directory to here:
+```sh
+pnpm build
+cd packages/backend/test-federation
+```
+
+First, you need to start servers by executing following commands:
+```sh
+bash ./setup.sh
+docker compose up --scale tester=0
+```
+
+Then you can run all tests by a following command:
+```sh
+docker compose run --no-deps --rm tester
+```
+
+For testing a specific file, run a following command:
+```sh
+docker compose run --no-deps --rm tester -- pnpm -F backend test:fed packages/backend/test-federation/test/user.test.ts
+```
diff --git a/packages/backend/test-federation/compose.a.yml b/packages/backend/test-federation/compose.a.yml
new file mode 100644
index 0000000000..6a305b404c
--- /dev/null
+++ b/packages/backend/test-federation/compose.a.yml
@@ -0,0 +1,64 @@
+services:
+  a.test:
+    extends:
+      file: ./compose.tpl.yml
+      service: nginx
+    depends_on:
+      misskey.a.test:
+        condition: service_healthy
+    networks:
+      - internal_network_a
+    volumes:
+      - type: bind
+        source: ./.config/a.test.conf
+        target: /etc/nginx/conf.d/a.test.conf
+        read_only: true
+      - type: bind
+        source: ./certificates/a.test.crt
+        target: /etc/nginx/certificates/a.test.crt
+        read_only: true
+      - type: bind
+        source: ./certificates/a.test.key
+        target: /etc/nginx/certificates/a.test.key
+        read_only: true
+
+  misskey.a.test:
+    extends:
+      file: ./compose.tpl.yml
+      service: misskey
+    depends_on:
+      db.a.test:
+        condition: service_healthy
+      redis.test:
+        condition: service_healthy
+      setup:
+        condition: service_completed_successfully
+    networks:
+      - internal_network_a
+    volumes:
+      - type: bind
+        source: ./.config/a.test.default.yml
+        target: /misskey/.config/default.yml
+        read_only: true
+
+  db.a.test:
+    extends:
+      file: ./compose.tpl.yml
+      service: db
+    networks:
+      - internal_network_a
+    volumes:
+      - type: bind
+        source: ./volumes/db.a
+        target: /var/lib/postgresql/data
+        bind:
+          create_host_path: true
+
+networks:
+  internal_network_a:
+    internal: true
+    driver: bridge
+    ipam:
+      config:
+        - subnet: 172.21.0.0/16
+          ip_range: 172.21.0.0/24
diff --git a/packages/backend/test-federation/compose.b.yml b/packages/backend/test-federation/compose.b.yml
new file mode 100644
index 0000000000..1158b53bae
--- /dev/null
+++ b/packages/backend/test-federation/compose.b.yml
@@ -0,0 +1,64 @@
+services:
+  b.test:
+    extends:
+      file: ./compose.tpl.yml
+      service: nginx
+    depends_on:
+      misskey.b.test:
+        condition: service_healthy
+    networks:
+      - internal_network_b
+    volumes:
+      - type: bind
+        source: ./.config/b.test.conf
+        target: /etc/nginx/conf.d/b.test.conf
+        read_only: true
+      - type: bind
+        source: ./certificates/b.test.crt
+        target: /etc/nginx/certificates/b.test.crt
+        read_only: true
+      - type: bind
+        source: ./certificates/b.test.key
+        target: /etc/nginx/certificates/b.test.key
+        read_only: true
+
+  misskey.b.test:
+    extends:
+      file: ./compose.tpl.yml
+      service: misskey
+    depends_on:
+      db.b.test:
+        condition: service_healthy
+      redis.test:
+        condition: service_healthy
+      setup:
+        condition: service_completed_successfully
+    networks:
+      - internal_network_b
+    volumes:
+      - type: bind
+        source: ./.config/b.test.default.yml
+        target: /misskey/.config/default.yml
+        read_only: true
+
+  db.b.test:
+    extends:
+      file: ./compose.tpl.yml
+      service: db
+    networks:
+      - internal_network_b
+    volumes:
+      - type: bind
+        source: ./volumes/db.b
+        target: /var/lib/postgresql/data
+        bind:
+          create_host_path: true
+
+networks:
+  internal_network_b:
+    internal: true
+    driver: bridge
+    ipam:
+      config:
+        - subnet: 172.22.0.0/16
+          ip_range: 172.22.0.0/24
diff --git a/packages/backend/test-federation/compose.override.yaml b/packages/backend/test-federation/compose.override.yaml
new file mode 100644
index 0000000000..60a7631ab5
--- /dev/null
+++ b/packages/backend/test-federation/compose.override.yaml
@@ -0,0 +1,117 @@
+services:
+  setup:
+    volumes:
+      - type: volume
+        source: node_modules
+        target: /misskey/node_modules
+      - type: volume
+        source: node_modules_backend
+        target: /misskey/packages/backend/node_modules
+      - type: volume
+        source: node_modules_misskey-js
+        target: /misskey/packages/misskey-js/node_modules
+      - type: volume
+        source: node_modules_misskey-reversi
+        target: /misskey/packages/misskey-reversi/node_modules
+
+  tester:
+    networks:
+      external_network:
+      internal_network:
+        ipv4_address: 172.20.1.1
+    volumes:
+      - type: volume
+        source: node_modules_dev
+        target: /misskey/node_modules
+      - type: volume
+        source: node_modules_backend_dev
+        target: /misskey/packages/backend/node_modules
+      - type: volume
+        source: node_modules_misskey-js_dev
+        target: /misskey/packages/misskey-js/node_modules
+
+  daemon:
+    networks:
+      - external_network
+      - internal_network_a
+      - internal_network_b
+    volumes:
+      - type: volume
+        source: node_modules_dev
+        target: /misskey/node_modules
+      - type: volume
+        source: node_modules_backend_dev
+        target: /misskey/packages/backend/node_modules
+
+  redis.test:
+    networks:
+      - internal_network_a
+      - internal_network_b
+
+  a.test:
+    networks:
+      - internal_network
+
+  misskey.a.test:
+    networks:
+      - external_network
+      - internal_network
+    volumes:
+      - type: volume
+        source: node_modules
+        target: /misskey/node_modules
+      - type: volume
+        source: node_modules_backend
+        target: /misskey/packages/backend/node_modules
+      - type: volume
+        source: node_modules_misskey-js
+        target: /misskey/packages/misskey-js/node_modules
+      - type: volume
+        source: node_modules_misskey-reversi
+        target: /misskey/packages/misskey-reversi/node_modules
+
+  b.test:
+    networks:
+      - internal_network
+
+  misskey.b.test:
+    networks:
+      - external_network
+      - internal_network
+    volumes:
+      - type: volume
+        source: node_modules
+        target: /misskey/node_modules
+      - type: volume
+        source: node_modules_backend
+        target: /misskey/packages/backend/node_modules
+      - type: volume
+        source: node_modules_misskey-js
+        target: /misskey/packages/misskey-js/node_modules
+      - type: volume
+        source: node_modules_misskey-reversi
+        target: /misskey/packages/misskey-reversi/node_modules
+
+networks:
+  external_network:
+    driver: bridge
+    ipam:
+      config:
+        - subnet: 172.23.0.0/16
+          ip_range: 172.23.0.0/24
+  internal_network:
+    internal: true
+    driver: bridge
+    ipam:
+      config:
+        - subnet: 172.20.0.0/16
+          ip_range: 172.20.0.0/24
+
+volumes:
+  node_modules:
+  node_modules_dev:
+  node_modules_backend:
+  node_modules_backend_dev:
+  node_modules_misskey-js:
+  node_modules_misskey-js_dev:
+  node_modules_misskey-reversi:
diff --git a/packages/backend/test-federation/compose.tpl.yml b/packages/backend/test-federation/compose.tpl.yml
new file mode 100644
index 0000000000..8c38f16919
--- /dev/null
+++ b/packages/backend/test-federation/compose.tpl.yml
@@ -0,0 +1,101 @@
+services:
+  nginx:
+    image: nginx:1.27
+    volumes:
+      - type: bind
+        source: ./certificates/rootCA.crt
+        target: /etc/nginx/certificates/rootCA.crt
+        read_only: true
+    healthcheck:
+      test: service nginx status
+      interval: 5s
+      retries: 20
+
+  misskey:
+    image: node:20
+    env_file:
+      - ./.config/docker.env
+    environment:
+      - NODE_ENV=production
+    volumes:
+      - type: bind
+        source: ../../../built
+        target: /misskey/built
+        read_only: true
+      - type: bind
+        source: ../assets
+        target: /misskey/packages/backend/assets
+        read_only: true
+      - type: bind
+        source: ../built
+        target: /misskey/packages/backend/built
+        read_only: true
+      - type: bind
+        source: ../migration
+        target: /misskey/packages/backend/migration
+        read_only: true
+      - type: bind
+        source: ../ormconfig.js
+        target: /misskey/packages/backend/ormconfig.js
+        read_only: true
+      - type: bind
+        source: ../package.json
+        target: /misskey/packages/backend/package.json
+        read_only: true
+      - type: bind
+        source: ../../misskey-js/built
+        target: /misskey/packages/misskey-js/built
+        read_only: true
+      - type: bind
+        source: ../../misskey-js/package.json
+        target: /misskey/packages/misskey-js/package.json
+        read_only: true
+      - type: bind
+        source: ../../misskey-reversi/built
+        target: /misskey/packages/misskey-reversi/built
+        read_only: true
+      - type: bind
+        source: ../../misskey-reversi/package.json
+        target: /misskey/packages/misskey-reversi/package.json
+        read_only: true
+      - type: bind
+        source: ../../../healthcheck.sh
+        target: /misskey/healthcheck.sh
+        read_only: true
+      - type: bind
+        source: ../../../package.json
+        target: /misskey/package.json
+        read_only: true
+      - type: bind
+        source: ../../../pnpm-lock.yaml
+        target: /misskey/pnpm-lock.yaml
+        read_only: true
+      - type: bind
+        source: ../../../pnpm-workspace.yaml
+        target: /misskey/pnpm-workspace.yaml
+        read_only: true
+      - type: bind
+        source: ./certificates/rootCA.crt
+        target: /usr/local/share/ca-certificates/rootCA.crt
+        read_only: true
+    working_dir: /misskey
+    command: >
+      bash -c "
+        corepack enable && corepack prepare
+        pnpm -F backend migrate
+        pnpm -F backend start
+      "
+    healthcheck:
+      test: bash /misskey/healthcheck.sh
+      interval: 5s
+      retries: 20
+
+  db:
+    image: postgres:15-alpine
+    env_file:
+      - ./.config/docker.env
+    volumes:
+    healthcheck:
+      test: pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB
+      interval: 5s
+      retries: 20
diff --git a/packages/backend/test-federation/compose.yml b/packages/backend/test-federation/compose.yml
new file mode 100644
index 0000000000..62d7e977c0
--- /dev/null
+++ b/packages/backend/test-federation/compose.yml
@@ -0,0 +1,133 @@
+include:
+  - ./compose.a.yml
+  - ./compose.b.yml
+
+services:
+  setup:
+    extends:
+      file: ./compose.tpl.yml
+      service: misskey
+    command: >
+      bash -c "
+        corepack enable && corepack prepare
+        pnpm -F backend i
+        pnpm -F misskey-js i
+        pnpm -F misskey-reversi i
+      "
+
+  tester:
+    image: node:20
+    depends_on:
+      a.test:
+        condition: service_healthy
+      b.test:
+        condition: service_healthy
+    environment:
+      - NODE_ENV=development
+      - NODE_EXTRA_CA_CERTS=/usr/local/share/ca-certificates/rootCA.crt
+    volumes:
+      - type: bind
+        source: ../package.json
+        target: /misskey/packages/backend/package.json
+        read_only: true
+      - type: bind
+        source: ../test/resources
+        target: /misskey/packages/backend/test/resources
+        read_only: true
+      - type: bind
+        source: ./test
+        target: /misskey/packages/backend/test-federation/test
+        read_only: true
+      - type: bind
+        source: ../jest.config.cjs
+        target: /misskey/packages/backend/jest.config.cjs
+        read_only: true
+      - type: bind
+        source: ../jest.config.fed.cjs
+        target: /misskey/packages/backend/jest.config.fed.cjs
+        read_only: true
+      - type: bind
+        source: ../../misskey-js/built
+        target: /misskey/packages/misskey-js/built
+        read_only: true
+      - type: bind
+        source: ../../misskey-js/package.json
+        target: /misskey/packages/misskey-js/package.json
+        read_only: true
+      - type: bind
+        source: ../../../package.json
+        target: /misskey/package.json
+        read_only: true
+      - type: bind
+        source: ../../../pnpm-lock.yaml
+        target: /misskey/pnpm-lock.yaml
+        read_only: true
+      - type: bind
+        source: ../../../pnpm-workspace.yaml
+        target: /misskey/pnpm-workspace.yaml
+        read_only: true
+      - type: bind
+        source: ./certificates/rootCA.crt
+        target: /usr/local/share/ca-certificates/rootCA.crt
+        read_only: true
+    working_dir: /misskey
+    entrypoint: >
+      bash -c '
+        corepack enable && corepack prepare
+        pnpm -F misskey-js i --frozen-lockfile
+        pnpm -F backend i --frozen-lockfile
+        exec "$0" "$@"
+      '
+    command: pnpm -F backend test:fed
+
+  daemon:
+    image: node:20
+    depends_on:
+      redis.test:
+        condition: service_healthy
+    volumes:
+      - type: bind
+        source: ../package.json
+        target: /misskey/packages/backend/package.json
+        read_only: true
+      - type: bind
+        source: ./daemon.ts
+        target: /misskey/packages/backend/test-federation/daemon.ts
+        read_only: true
+      - type: bind
+        source: ./tsconfig.json
+        target: /misskey/packages/backend/test-federation/tsconfig.json
+        read_only: true
+      - type: bind
+        source: ../../../package.json
+        target: /misskey/package.json
+        read_only: true
+      - type: bind
+        source: ../../../pnpm-lock.yaml
+        target: /misskey/pnpm-lock.yaml
+        read_only: true
+      - type: bind
+        source: ../../../pnpm-workspace.yaml
+        target: /misskey/pnpm-workspace.yaml
+        read_only: true
+    working_dir: /misskey
+    command: >
+      bash -c "
+        corepack enable && corepack prepare
+        pnpm -F backend i --frozen-lockfile
+        pnpm exec tsc -p ./packages/backend/test-federation
+        node ./packages/backend/test-federation/built/daemon.js
+      "
+
+  redis.test:
+    image: redis:7-alpine
+    volumes:
+      - type: bind
+        source: ./volumes/redis
+        target: /data
+        bind:
+          create_host_path: true
+    healthcheck:
+      test: redis-cli ping
+      interval: 5s
+      retries: 20
diff --git a/packages/backend/test-federation/daemon.ts b/packages/backend/test-federation/daemon.ts
new file mode 100644
index 0000000000..46b6963c79
--- /dev/null
+++ b/packages/backend/test-federation/daemon.ts
@@ -0,0 +1,38 @@
+import IPCIDR from 'ip-cidr';
+import { Redis } from 'ioredis';
+
+const TESTER_IP_ADDRESS = '172.20.1.1';
+
+/**
+ * This should be same as {@link file://./../src/misc/get-ip-hash.ts}.
+ */
+function getIpHash(ip: string) {
+	const prefix = IPCIDR.createAddress(ip).mask(64);
+	return `ip-${BigInt('0b' + prefix).toString(36)}`;
+}
+
+/**
+ * This prevents hitting rate limit when login.
+ */
+export async function purgeLimit(host: string, client: Redis) {
+	const ipHash = getIpHash(TESTER_IP_ADDRESS);
+	const key = `${host}:limit:${ipHash}:signin`;
+	const res = await client.zrange(key, 0, -1);
+	if (res.length !== 0) {
+		console.log(`${key} - ${JSON.stringify(res)}`);
+		await client.del(key);
+	}
+}
+
+console.log('Daemon started running');
+
+{
+	const redisClient = new Redis({
+		host: 'redis.test',
+	});
+
+	setInterval(() => {
+		purgeLimit('a.test', redisClient);
+		purgeLimit('b.test', redisClient);
+	}, 200);
+}
diff --git a/packages/backend/test-federation/eslint.config.js b/packages/backend/test-federation/eslint.config.js
new file mode 100644
index 0000000000..e3bcf4c0fe
--- /dev/null
+++ b/packages/backend/test-federation/eslint.config.js
@@ -0,0 +1,21 @@
+import globals from 'globals';
+import tsParser from '@typescript-eslint/parser';
+import sharedConfig from '../../shared/eslint.config.js';
+
+export default [
+	...sharedConfig,
+	{
+		files: ['**/*.ts', '**/*.tsx'],
+		languageOptions: {
+			globals: {
+				...globals.node,
+			},
+			parserOptions: {
+				parser: tsParser,
+				project: ['./tsconfig.json'],
+				sourceType: 'module',
+				tsconfigRootDir: import.meta.dirname,
+			},
+		},
+	},
+];
diff --git a/packages/backend/test-federation/setup.sh b/packages/backend/test-federation/setup.sh
new file mode 100644
index 0000000000..1bc3a2a87c
--- /dev/null
+++ b/packages/backend/test-federation/setup.sh
@@ -0,0 +1,35 @@
+#!/bin/bash
+mkdir certificates
+
+# rootCA
+openssl genrsa -des3 \
+  -passout pass:rootCA \
+  -out certificates/rootCA.key 4096
+openssl req -x509 -new -nodes -batch \
+  -key certificates/rootCA.key \
+  -sha256 \
+  -days 1024 \
+  -passin pass:rootCA \
+  -out certificates/rootCA.crt
+
+# domain
+function generate {
+  openssl req -new -newkey rsa:2048 -sha256 -nodes \
+    -keyout certificates/$1.key \
+    -subj "/CN=$1/emailAddress=admin@$1/C=JP/ST=/L=/O=Misskey Tester/OU=Some Unit" \
+    -out certificates/$1.csr
+  openssl x509 -req -sha256 \
+    -in certificates/$1.csr \
+    -CA certificates/rootCA.crt \
+    -CAkey certificates/rootCA.key \
+    -CAcreateserial \
+    -passin pass:rootCA \
+    -out certificates/$1.crt \
+    -days 500
+  if [ ! -f .config/docker.env ]; then cp .config/example.docker.env .config/docker.env; fi
+  if [ ! -f .config/$1.conf ]; then sed "s/\${HOST}/$1/g" .config/example.conf > .config/$1.conf; fi
+  if [ ! -f .config/$1.default.yml ]; then sed "s/\${HOST}/$1/g" .config/example.default.yml > .config/$1.default.yml; fi
+}
+
+generate a.test
+generate b.test
diff --git a/packages/backend/test-federation/test/abuse-report.test.ts b/packages/backend/test-federation/test/abuse-report.test.ts
new file mode 100644
index 0000000000..b54d6222b4
--- /dev/null
+++ b/packages/backend/test-federation/test/abuse-report.test.ts
@@ -0,0 +1,52 @@
+import { rejects, strictEqual } from 'node:assert';
+import * as Misskey from 'misskey-js';
+import { createAccount, createModerator, resolveRemoteUser, sleep, type LoginUser } from './utils.js';
+
+describe('Abuse report', () => {
+	describe('Forwarding report', () => {
+		let alice: LoginUser, bob: LoginUser, aModerator: LoginUser, bModerator: LoginUser;
+		let bobInA: Misskey.entities.UserDetailedNotMe, aliceInB: Misskey.entities.UserDetailedNotMe;
+
+		beforeAll(async () => {
+			[alice, bob] = await Promise.all([
+				createAccount('a.test'),
+				createAccount('b.test'),
+			]);
+
+			[aModerator, bModerator] = await Promise.all([
+				createModerator('a.test'),
+				createModerator('b.test'),
+			]);
+
+			[bobInA, aliceInB] = await Promise.all([
+				resolveRemoteUser('b.test', bob.id, alice),
+				resolveRemoteUser('a.test', alice.id, bob),
+			]);
+		});
+
+		test('Alice reports Bob, moderator in A forwards it, and B moderator receives it', async () => {
+			const comment = crypto.randomUUID();
+			await alice.client.request('users/report-abuse', { userId: bobInA.id, comment });
+			const reports = await aModerator.client.request('admin/abuse-user-reports', {});
+			const report = reports.filter(report => report.comment === comment)[0];
+			await aModerator.client.request('admin/forward-abuse-user-report', { reportId: report.id });
+			await sleep();
+
+			const reportsInB = await bModerator.client.request('admin/abuse-user-reports', {});
+			const reportInB = reportsInB.filter(report => report.comment.includes(comment))[0];
+			// NOTE: reporter is not Alice, and is not moderator in A
+			strictEqual(reportInB.reporter.url, 'https://a.test/@instance.actor');
+			strictEqual(reportInB.targetUserId, bob.id);
+
+			// NOTE: cannot forward multiple times
+			await rejects(
+				async () => await aModerator.client.request('admin/forward-abuse-user-report', { reportId: report.id }),
+				(err: any) => {
+					strictEqual(err.code, 'INTERNAL_ERROR');
+					strictEqual(err.info.e.message, 'The report has already been forwarded.');
+					return true;
+				},
+			);
+		});
+	});
+});
diff --git a/packages/backend/test-federation/test/block.test.ts b/packages/backend/test-federation/test/block.test.ts
new file mode 100644
index 0000000000..ef910eeaea
--- /dev/null
+++ b/packages/backend/test-federation/test/block.test.ts
@@ -0,0 +1,224 @@
+import { deepStrictEqual, rejects, strictEqual } from 'node:assert';
+import * as Misskey from 'misskey-js';
+import { assertNotificationReceived, createAccount, type LoginUser, resolveRemoteNote, resolveRemoteUser, sleep } from './utils.js';
+
+describe('Block', () => {
+	describe('Check follow', () => {
+		let alice: LoginUser, bob: LoginUser;
+		let bobInA: Misskey.entities.UserDetailedNotMe, aliceInB: Misskey.entities.UserDetailedNotMe;
+
+		beforeAll(async () => {
+			[alice, bob] = await Promise.all([
+				createAccount('a.test'),
+				createAccount('b.test'),
+			]);
+
+			[bobInA, aliceInB] = await Promise.all([
+				resolveRemoteUser('b.test', bob.id, alice),
+				resolveRemoteUser('a.test', alice.id, bob),
+			]);
+		});
+
+		test('Cannot follow if blocked', async () => {
+			await alice.client.request('blocking/create', { userId: bobInA.id });
+			await sleep();
+			await rejects(
+				async () => await bob.client.request('following/create', { userId: aliceInB.id }),
+				(err: any) => {
+					strictEqual(err.code, 'BLOCKED');
+					return true;
+				},
+			);
+
+			const following = await bob.client.request('users/following', { userId: bob.id });
+			strictEqual(following.length, 0);
+			const followers = await alice.client.request('users/followers', { userId: alice.id });
+			strictEqual(followers.length, 0);
+		});
+
+		// FIXME: this is invalid case
+		test('Cannot follow even if unblocked', async () => {
+			// unblock here
+			await alice.client.request('blocking/delete', { userId: bobInA.id });
+			await sleep();
+
+			// TODO: why still being blocked?
+			await rejects(
+				async () => await bob.client.request('following/create', { userId: aliceInB.id }),
+				(err: any) => {
+					strictEqual(err.code, 'BLOCKED');
+					return true;
+				},
+			);
+		});
+
+		test.skip('Can follow if unblocked', async () => {
+			await alice.client.request('blocking/delete', { userId: bobInA.id });
+			await sleep();
+
+			await bob.client.request('following/create', { userId: aliceInB.id });
+			await sleep();
+
+			const following = await bob.client.request('users/following', { userId: bob.id });
+			strictEqual(following.length, 1);
+			const followers = await alice.client.request('users/followers', { userId: alice.id });
+			strictEqual(followers.length, 1);
+		});
+
+		test.skip('Remove follower when block them', async () => {
+			test('before block', async () => {
+				const following = await bob.client.request('users/following', { userId: bob.id });
+				strictEqual(following.length, 1);
+				const followers = await alice.client.request('users/followers', { userId: alice.id });
+				strictEqual(followers.length, 1);
+			});
+
+			await alice.client.request('blocking/create', { userId: bobInA.id });
+			await sleep();
+
+			test('after block', async () => {
+				const following = await bob.client.request('users/following', { userId: bob.id });
+				strictEqual(following.length, 0);
+				const followers = await alice.client.request('users/followers', { userId: alice.id });
+				strictEqual(followers.length, 0);
+			});
+		});
+	});
+
+	describe('Check reply', () => {
+		let alice: LoginUser, bob: LoginUser;
+		let bobInA: Misskey.entities.UserDetailedNotMe, aliceInB: Misskey.entities.UserDetailedNotMe;
+
+		beforeAll(async () => {
+			[alice, bob] = await Promise.all([
+				createAccount('a.test'),
+				createAccount('b.test'),
+			]);
+
+			[bobInA, aliceInB] = await Promise.all([
+				resolveRemoteUser('b.test', bob.id, alice),
+				resolveRemoteUser('a.test', alice.id, bob),
+			]);
+		});
+
+		test('Cannot reply if blocked', async () => {
+			await alice.client.request('blocking/create', { userId: bobInA.id });
+			await sleep();
+
+			const note = (await alice.client.request('notes/create', { text: 'a' })).createdNote;
+			const resolvedNote = await resolveRemoteNote('a.test', note.id, bob);
+			await rejects(
+				async () => await bob.client.request('notes/create', { text: 'b', replyId: resolvedNote.id }),
+				(err: any) => {
+					strictEqual(err.code, 'YOU_HAVE_BEEN_BLOCKED');
+					return true;
+				},
+			);
+		});
+
+		test('Can reply if unblocked', async () => {
+			await alice.client.request('blocking/delete', { userId: bobInA.id });
+			await sleep();
+
+			const note = (await alice.client.request('notes/create', { text: 'a' })).createdNote;
+			const resolvedNote = await resolveRemoteNote('a.test', note.id, bob);
+			const reply = (await bob.client.request('notes/create', { text: 'b', replyId: resolvedNote.id })).createdNote;
+
+			await resolveRemoteNote('b.test', reply.id, alice);
+		});
+	});
+
+	describe('Check reaction', () => {
+		let alice: LoginUser, bob: LoginUser;
+		let bobInA: Misskey.entities.UserDetailedNotMe, aliceInB: Misskey.entities.UserDetailedNotMe;
+
+		beforeAll(async () => {
+			[alice, bob] = await Promise.all([
+				createAccount('a.test'),
+				createAccount('b.test'),
+			]);
+
+			[bobInA, aliceInB] = await Promise.all([
+				resolveRemoteUser('b.test', bob.id, alice),
+				resolveRemoteUser('a.test', alice.id, bob),
+			]);
+		});
+
+		test('Cannot reaction if blocked', async () => {
+			await alice.client.request('blocking/create', { userId: bobInA.id });
+			await sleep();
+
+			const note = (await alice.client.request('notes/create', { text: 'a' })).createdNote;
+			const resolvedNote = await resolveRemoteNote('a.test', note.id, bob);
+			await rejects(
+				async () => await bob.client.request('notes/reactions/create', { noteId: resolvedNote.id, reaction: '😅' }),
+				(err: any) => {
+					strictEqual(err.code, 'YOU_HAVE_BEEN_BLOCKED');
+					return true;
+				},
+			);
+		});
+
+		// FIXME: this is invalid case
+		test('Cannot reaction even if unblocked', async () => {
+			// unblock here
+			await alice.client.request('blocking/delete', { userId: bobInA.id });
+			await sleep();
+
+			const note = (await alice.client.request('notes/create', { text: 'a' })).createdNote;
+			const resolvedNote = await resolveRemoteNote('a.test', note.id, bob);
+
+			// TODO: why still being blocked?
+			await rejects(
+				async () => await bob.client.request('notes/reactions/create', { noteId: resolvedNote.id, reaction: '😅' }),
+				(err: any) => {
+					strictEqual(err.code, 'YOU_HAVE_BEEN_BLOCKED');
+					return true;
+				},
+			);
+		});
+
+		test.skip('Can reaction if unblocked', async () => {
+			await alice.client.request('blocking/delete', { userId: bobInA.id });
+			await sleep();
+
+			const note = (await alice.client.request('notes/create', { text: 'a' })).createdNote;
+			const resolvedNote = await resolveRemoteNote('a.test', note.id, bob);
+			await bob.client.request('notes/reactions/create', { noteId: resolvedNote.id, reaction: '😅' });
+
+			const _note = await alice.client.request('notes/show', { noteId: note.id });
+			deepStrictEqual(_note.reactions, { '😅': 1 });
+		});
+	});
+
+	describe('Check mention', () => {
+		let alice: LoginUser, bob: LoginUser;
+		let bobInA: Misskey.entities.UserDetailedNotMe, aliceInB: Misskey.entities.UserDetailedNotMe;
+
+		beforeAll(async () => {
+			[alice, bob] = await Promise.all([
+				createAccount('a.test'),
+				createAccount('b.test'),
+			]);
+
+			[bobInA, aliceInB] = await Promise.all([
+				resolveRemoteUser('b.test', bob.id, alice),
+				resolveRemoteUser('a.test', alice.id, bob),
+			]);
+		});
+
+		/** NOTE: You should mute the target to stop receiving notifications */
+		test('Can mention and notified even if blocked', async () => {
+			await alice.client.request('blocking/create', { userId: bobInA.id });
+			await sleep();
+
+			const text = `@${alice.username}@a.test plz unblock me!`;
+			await assertNotificationReceived(
+				'a.test', alice,
+				async () => await bob.client.request('notes/create', { text }),
+				notification => notification.type === 'mention' && notification.userId === bobInA.id && notification.note.text === text,
+				true,
+			);
+		});
+	});
+});
diff --git a/packages/backend/test-federation/test/drive.test.ts b/packages/backend/test-federation/test/drive.test.ts
new file mode 100644
index 0000000000..f755183b4d
--- /dev/null
+++ b/packages/backend/test-federation/test/drive.test.ts
@@ -0,0 +1,175 @@
+import assert, { strictEqual } from 'node:assert';
+import * as Misskey from 'misskey-js';
+import { createAccount, deepStrictEqualWithExcludedFields, fetchAdmin, type LoginUser, resolveRemoteNote, resolveRemoteUser, sleep, uploadFile } from './utils.js';
+
+const bAdmin = await fetchAdmin('b.test');
+
+describe('Drive', () => {
+	describe('Upload image in a.test and resolve from b.test', () => {
+		let uploader: LoginUser;
+
+		beforeAll(async () => {
+			uploader = await createAccount('a.test');
+		});
+
+		let image: Misskey.entities.DriveFile, imageInB: Misskey.entities.DriveFile;
+
+		describe('Upload', () => {
+			beforeAll(async () => {
+				image = await uploadFile('a.test', uploader);
+				const noteWithImage = (await uploader.client.request('notes/create', { fileIds: [image.id] })).createdNote;
+				const noteInB = await resolveRemoteNote('a.test', noteWithImage.id, bAdmin);
+				assert(noteInB.files != null);
+				strictEqual(noteInB.files.length, 1);
+				imageInB = noteInB.files[0];
+			});
+
+			test('Check consistency of DriveFile', () => {
+				// console.log(`a.test: ${JSON.stringify(image, null, '\t')}`);
+				// console.log(`b.test: ${JSON.stringify(imageInB, null, '\t')}`);
+
+				deepStrictEqualWithExcludedFields(image, imageInB, [
+					'id',
+					'createdAt',
+					'size',
+					'url',
+					'thumbnailUrl',
+					'userId',
+				]);
+			});
+		});
+
+		let updatedImage: Misskey.entities.DriveFile, updatedImageInB: Misskey.entities.DriveFile;
+
+		describe('Update', () => {
+			beforeAll(async () => {
+				updatedImage = await uploader.client.request('drive/files/update', {
+					fileId: image.id,
+					name: 'updated_192.jpg',
+					isSensitive: true,
+				});
+
+				updatedImageInB = await bAdmin.client.request('drive/files/show', {
+					fileId: imageInB.id,
+				});
+			});
+
+			test('Check consistency', () => {
+				// console.log(`a.test: ${JSON.stringify(updatedImage, null, '\t')}`);
+				// console.log(`b.test: ${JSON.stringify(updatedImageInB, null, '\t')}`);
+
+				// FIXME: not updated with `drive/files/update`
+				strictEqual(updatedImage.isSensitive, true);
+				strictEqual(updatedImage.name, 'updated_192.jpg');
+				strictEqual(updatedImageInB.isSensitive, false);
+				strictEqual(updatedImageInB.name, '192.jpg');
+			});
+		});
+
+		let reupdatedImageInB: Misskey.entities.DriveFile;
+
+		describe('Re-update with attaching to Note', () => {
+			beforeAll(async () => {
+				const noteWithUpdatedImage = (await uploader.client.request('notes/create', { fileIds: [updatedImage.id] })).createdNote;
+				const noteWithUpdatedImageInB = await resolveRemoteNote('a.test', noteWithUpdatedImage.id, bAdmin);
+				assert(noteWithUpdatedImageInB.files != null);
+				strictEqual(noteWithUpdatedImageInB.files.length, 1);
+				reupdatedImageInB = noteWithUpdatedImageInB.files[0];
+			});
+
+			test('Check consistency', () => {
+				// console.log(`b.test: ${JSON.stringify(reupdatedImageInB, null, '\t')}`);
+
+				// `isSensitive` is updated
+				strictEqual(reupdatedImageInB.isSensitive, true);
+				// FIXME: but `name` is not updated
+				strictEqual(reupdatedImageInB.name, '192.jpg');
+			});
+		});
+	});
+
+	describe('Sensitive flag', () => {
+		describe('isSensitive is federated in delivering to followers', () => {
+			let alice: LoginUser, bob: LoginUser;
+			let bobInA: Misskey.entities.UserDetailedNotMe, aliceInB: Misskey.entities.UserDetailedNotMe;
+
+			beforeAll(async () => {
+				[alice, bob] = await Promise.all([
+					createAccount('a.test'),
+					createAccount('b.test'),
+				]);
+
+				[bobInA, aliceInB] = await Promise.all([
+					resolveRemoteUser('b.test', bob.id, alice),
+					resolveRemoteUser('a.test', alice.id, bob),
+				]);
+
+				await bob.client.request('following/create', { userId: aliceInB.id });
+				await sleep();
+			});
+
+			test('Alice uploads sensitive image and it is shown as sensitive from Bob', async () => {
+				const file = await uploadFile('a.test', alice);
+				await alice.client.request('drive/files/update', { fileId: file.id, isSensitive: true });
+				await alice.client.request('notes/create', { text: 'sensitive', fileIds: [file.id] });
+				await sleep();
+
+				const notes = await bob.client.request('notes/timeline', {});
+				strictEqual(notes.length, 1);
+				const noteInB = notes[0];
+				assert(noteInB.files != null);
+				strictEqual(noteInB.files.length, 1);
+				strictEqual(noteInB.files[0].isSensitive, true);
+			});
+		});
+
+		describe('isSensitive is federated in resolving', () => {
+			let alice: LoginUser, bob: LoginUser;
+
+			beforeAll(async () => {
+				[alice, bob] = await Promise.all([
+					createAccount('a.test'),
+					createAccount('b.test'),
+				]);
+			});
+
+			test('Alice uploads sensitive image and it is shown as sensitive from Bob', async () => {
+				const file = await uploadFile('a.test', alice);
+				await alice.client.request('drive/files/update', { fileId: file.id, isSensitive: true });
+				const note = (await alice.client.request('notes/create', { text: 'sensitive', fileIds: [file.id] })).createdNote;
+
+				const noteInB = await resolveRemoteNote('a.test', note.id, bob);
+				assert(noteInB.files != null);
+				strictEqual(noteInB.files.length, 1);
+				strictEqual(noteInB.files[0].isSensitive, true);
+			});
+		});
+
+		/** @see https://github.com/misskey-dev/misskey/issues/12208 */
+		describe('isSensitive is federated in replying', () => {
+			let alice: LoginUser, bob: LoginUser;
+
+			beforeAll(async () => {
+				[alice, bob] = await Promise.all([
+					createAccount('a.test'),
+					createAccount('b.test'),
+				]);
+			});
+
+			test('Alice uploads sensitive image and it is shown as sensitive from Bob', async () => {
+				const bobNote = (await bob.client.request('notes/create', { text: 'I\'m Bob' })).createdNote;
+
+				const file = await uploadFile('a.test', alice);
+				await alice.client.request('drive/files/update', { fileId: file.id, isSensitive: true });
+				const bobNoteInA = await resolveRemoteNote('b.test', bobNote.id, alice);
+				const note = (await alice.client.request('notes/create', { text: 'sensitive', fileIds: [file.id], replyId: bobNoteInA.id })).createdNote;
+				await sleep();
+
+				const noteInB = await resolveRemoteNote('a.test', note.id, bob);
+				assert(noteInB.files != null);
+				strictEqual(noteInB.files.length, 1);
+				strictEqual(noteInB.files[0].isSensitive, true);
+			});
+		});
+	});
+});
diff --git a/packages/backend/test-federation/test/emoji.test.ts b/packages/backend/test-federation/test/emoji.test.ts
new file mode 100644
index 0000000000..3119ca6e4d
--- /dev/null
+++ b/packages/backend/test-federation/test/emoji.test.ts
@@ -0,0 +1,97 @@
+import assert, { deepStrictEqual, strictEqual } from 'assert';
+import * as Misskey from 'misskey-js';
+import { addCustomEmoji, createAccount, type LoginUser, resolveRemoteUser, sleep } from './utils.js';
+
+describe('Emoji', () => {
+	let alice: LoginUser, bob: LoginUser;
+	let bobInA: Misskey.entities.UserDetailedNotMe, aliceInB: Misskey.entities.UserDetailedNotMe;
+
+	beforeAll(async () => {
+		[alice, bob] = await Promise.all([
+			createAccount('a.test'),
+			createAccount('b.test'),
+		]);
+
+		[bobInA, aliceInB] = await Promise.all([
+			resolveRemoteUser('b.test', bob.id, alice),
+			resolveRemoteUser('a.test', alice.id, bob),
+		]);
+
+		await bob.client.request('following/create', { userId: aliceInB.id });
+		await sleep();
+	});
+
+	test('Custom emoji are delivered with Note delivery', async () => {
+		const emoji = await addCustomEmoji('a.test');
+		await alice.client.request('notes/create', { text: `I love :${emoji.name}:` });
+		await sleep();
+
+		const notes = await bob.client.request('notes/timeline', {});
+		const noteInB = notes[0];
+
+		strictEqual(noteInB.text, `I love \u200b:${emoji.name}:\u200b`);
+		assert(noteInB.emojis != null);
+		assert(emoji.name in noteInB.emojis);
+		strictEqual(noteInB.emojis[emoji.name], emoji.url);
+	});
+
+	test('Custom emoji are delivered with Reaction delivery', async () => {
+		const emoji = await addCustomEmoji('a.test');
+		const note = (await alice.client.request('notes/create', { text: 'a' })).createdNote;
+		await sleep();
+
+		await alice.client.request('notes/reactions/create', { noteId: note.id, reaction: `:${emoji.name}:` });
+		await sleep();
+
+		const noteInB = (await bob.client.request('notes/timeline', {}))[0];
+		deepStrictEqual(noteInB.reactions[`:${emoji.name}@a.test:`], 1);
+		deepStrictEqual(noteInB.reactionEmojis[`${emoji.name}@a.test`], emoji.url);
+	});
+
+	test('Custom emoji are delivered with Profile delivery', async () => {
+		const emoji = await addCustomEmoji('a.test');
+		const renewedAlice = await alice.client.request('i/update', { name: `:${emoji.name}:` });
+		await sleep();
+
+		const renewedaliceInB = await bob.client.request('users/show', { userId: aliceInB.id });
+		strictEqual(renewedaliceInB.name, renewedAlice.name);
+		assert(emoji.name in renewedaliceInB.emojis);
+		strictEqual(renewedaliceInB.emojis[emoji.name], emoji.url);
+	});
+
+	test('Local-only custom emoji aren\'t delivered with Note delivery', async () => {
+		const emoji = await addCustomEmoji('a.test', { localOnly: true });
+		await alice.client.request('notes/create', { text: `I love :${emoji.name}:` });
+		await sleep();
+
+		const notes = await bob.client.request('notes/timeline', {});
+		const noteInB = notes[0];
+
+		strictEqual(noteInB.text, `I love \u200b:${emoji.name}:\u200b`);
+		// deepStrictEqual(noteInB.emojis, {}); // TODO: this fails (why?)
+		deepStrictEqual({ ...noteInB.emojis }, {});
+	});
+
+	test('Local-only custom emoji aren\'t delivered with Reaction delivery', async () => {
+		const emoji = await addCustomEmoji('a.test', { localOnly: true });
+		const note = (await alice.client.request('notes/create', { text: 'a' })).createdNote;
+		await sleep();
+
+		await alice.client.request('notes/reactions/create', { noteId: note.id, reaction: `:${emoji.name}:` });
+		await sleep();
+
+		const noteInB = (await bob.client.request('notes/timeline', {}))[0];
+		deepStrictEqual({ ...noteInB.reactions }, { '❤': 1 });
+		deepStrictEqual({ ...noteInB.reactionEmojis }, {});
+	});
+
+	test('Local-only custom emoji aren\'t delivered with Profile delivery', async () => {
+		const emoji = await addCustomEmoji('a.test', { localOnly: true });
+		const renewedAlice = await alice.client.request('i/update', { name: `:${emoji.name}:` });
+		await sleep();
+
+		const renewedaliceInB = await bob.client.request('users/show', { userId: aliceInB.id });
+		strictEqual(renewedaliceInB.name, renewedAlice.name);
+		deepStrictEqual({ ...renewedaliceInB.emojis }, {});
+	});
+});
diff --git a/packages/backend/test-federation/test/move.test.ts b/packages/backend/test-federation/test/move.test.ts
new file mode 100644
index 0000000000..56a57de8a4
--- /dev/null
+++ b/packages/backend/test-federation/test/move.test.ts
@@ -0,0 +1,52 @@
+import assert, { strictEqual } from 'node:assert';
+import { createAccount, type LoginUser, sleep } from './utils.js';
+
+describe('Move', () => {
+	test('Minimum move', async () => {
+		const [alice, bob] = await Promise.all([
+			createAccount('a.test'),
+			createAccount('b.test'),
+		]);
+
+		await bob.client.request('i/update', { alsoKnownAs: [`@${alice.username}@a.test`] });
+		await alice.client.request('i/move', { moveToAccount: `@${bob.username}@b.test` });
+	});
+
+	/** @see https://github.com/misskey-dev/misskey/issues/11320 */
+	describe('Following relation is transferred after move', () => {
+		let alice: LoginUser, bob: LoginUser, carol: LoginUser;
+
+		beforeAll(async () => {
+			[alice, bob] = await Promise.all([
+				createAccount('a.test'),
+				createAccount('b.test'),
+			]);
+			carol = await createAccount('a.test');
+
+			// Follow @carol@a.test ==> @alice@a.test
+			await carol.client.request('following/create', { userId: alice.id });
+
+			// Move @alice@a.test ==> @bob@b.test
+			await bob.client.request('i/update', { alsoKnownAs: [`@${alice.username}@a.test`] });
+			await alice.client.request('i/move', { moveToAccount: `@${bob.username}@b.test` });
+			await sleep();
+		});
+
+		test('Check from follower', async () => {
+			const following = await carol.client.request('users/following', { userId: carol.id });
+			strictEqual(following.length, 2);
+			const followees = following.map(({ followee }) => followee);
+			assert(followees.every(followee => followee != null));
+			assert(followees.some(({ id, url }) => id === alice.id && url === null));
+			assert(followees.some(({ url }) => url === `https://b.test/@${bob.username}`));
+		});
+
+		test('Check from followee', async () => {
+			const followers = await bob.client.request('users/followers', { userId: bob.id });
+			strictEqual(followers.length, 1);
+			const follower = followers[0].follower;
+			assert(follower != null);
+			strictEqual(follower.url, `https://a.test/@${carol.username}`);
+		});
+	});
+});
diff --git a/packages/backend/test-federation/test/note.test.ts b/packages/backend/test-federation/test/note.test.ts
new file mode 100644
index 0000000000..bacc4cc54f
--- /dev/null
+++ b/packages/backend/test-federation/test/note.test.ts
@@ -0,0 +1,317 @@
+import assert, { rejects, strictEqual } from 'node:assert';
+import * as Misskey from 'misskey-js';
+import { addCustomEmoji, createAccount, createModerator, deepStrictEqualWithExcludedFields, type LoginUser, resolveRemoteNote, resolveRemoteUser, sleep, uploadFile } from './utils.js';
+
+describe('Note', () => {
+	let alice: LoginUser, bob: LoginUser;
+	let bobInA: Misskey.entities.UserDetailedNotMe, aliceInB: Misskey.entities.UserDetailedNotMe;
+
+	beforeAll(async () => {
+		[alice, bob] = await Promise.all([
+			createAccount('a.test'),
+			createAccount('b.test'),
+		]);
+
+		[bobInA, aliceInB] = await Promise.all([
+			resolveRemoteUser('b.test', bob.id, alice),
+			resolveRemoteUser('a.test', alice.id, bob),
+		]);
+	});
+
+	describe('Note content', () => {
+		test('Consistency of Public Note', async () => {
+			const image = await uploadFile('a.test', alice);
+			const note = (await alice.client.request('notes/create', {
+				text: 'I am Alice!',
+				fileIds: [image.id],
+				poll: {
+					choices: ['neko', 'inu'],
+					multiple: false,
+					expiredAfter: 60 * 60 * 1000,
+				},
+			})).createdNote;
+
+			const resolvedNote = await resolveRemoteNote('a.test', note.id, bob);
+			deepStrictEqualWithExcludedFields(note, resolvedNote, [
+				'id',
+				'emojis',
+				/** Consistency of files is checked at {@link file://./drive.test.ts}, so let's skip. */
+				'fileIds',
+				'files',
+				/** @see https://github.com/misskey-dev/misskey/issues/12409 */
+				'reactionAcceptance',
+				'userId',
+				'user',
+				'uri',
+			]);
+			strictEqual(aliceInB.id, resolvedNote.userId);
+		});
+
+		test('Consistency of reply', async () => {
+			const _replyedNote = (await alice.client.request('notes/create', {
+				text: 'a',
+			})).createdNote;
+			const note = (await alice.client.request('notes/create', {
+				text: 'b',
+				replyId: _replyedNote.id,
+			})).createdNote;
+			// NOTE: the repliedCount is incremented, so fetch again
+			const replyedNote = await alice.client.request('notes/show', { noteId: _replyedNote.id });
+			strictEqual(replyedNote.repliesCount, 1);
+
+			const resolvedNote = await resolveRemoteNote('a.test', note.id, bob);
+			deepStrictEqualWithExcludedFields(note, resolvedNote, [
+				'id',
+				'emojis',
+				'reactionAcceptance',
+				'replyId',
+				'reply',
+				'userId',
+				'user',
+				'uri',
+			]);
+			assert(resolvedNote.replyId != null);
+			assert(resolvedNote.reply != null);
+			deepStrictEqualWithExcludedFields(replyedNote, resolvedNote.reply, [
+				'id',
+				// TODO: why clippedCount loses consistency?
+				'clippedCount',
+				'emojis',
+				'userId',
+				'user',
+				'uri',
+				// flaky because this is parallelly incremented, so let's check it below
+				'repliesCount',
+			]);
+			strictEqual(aliceInB.id, resolvedNote.userId);
+
+			await sleep();
+
+			const resolvedReplyedNote = await bob.client.request('notes/show', { noteId: resolvedNote.replyId });
+			strictEqual(resolvedReplyedNote.repliesCount, 1);
+		});
+
+		test('Consistency of Renote', async () => {
+			// NOTE: the renoteCount is not incremented, so no need to fetch again
+			const renotedNote = (await alice.client.request('notes/create', {
+				text: 'a',
+			})).createdNote;
+			const note = (await alice.client.request('notes/create', {
+				text: 'b',
+				renoteId: renotedNote.id,
+			})).createdNote;
+
+			const resolvedNote = await resolveRemoteNote('a.test', note.id, bob);
+			deepStrictEqualWithExcludedFields(note, resolvedNote, [
+				'id',
+				'emojis',
+				'reactionAcceptance',
+				'renoteId',
+				'renote',
+				'userId',
+				'user',
+				'uri',
+			]);
+			assert(resolvedNote.renoteId != null);
+			assert(resolvedNote.renote != null);
+			deepStrictEqualWithExcludedFields(renotedNote, resolvedNote.renote, [
+				'id',
+				'emojis',
+				'userId',
+				'user',
+				'uri',
+			]);
+			strictEqual(aliceInB.id, resolvedNote.userId);
+		});
+	});
+
+	describe('Other props', () => {
+		test('localOnly', async () => {
+			const note = (await alice.client.request('notes/create', { text: 'a', localOnly: true })).createdNote;
+			rejects(
+				async () => await bob.client.request('ap/show', { uri: `https://a.test/notes/${note.id}` }),
+				(err: any) => {
+					/**
+					 * FIXME: this error is not handled
+					 * @see https://github.com/misskey-dev/misskey/issues/12736
+					 */
+					strictEqual(err.code, 'INTERNAL_ERROR');
+					return true;
+				},
+			);
+		});
+	});
+
+	describe('Deletion', () => {
+		describe('Check Delete consistency', () => {
+			let carol: LoginUser;
+
+			beforeAll(async () => {
+				carol = await createAccount('a.test');
+
+				await carol.client.request('following/create', { userId: bobInA.id });
+				await sleep();
+			});
+
+			test('Delete is derivered to followers', async () => {
+				const note = (await bob.client.request('notes/create', { text: 'I\'m Bob.' })).createdNote;
+				const noteInA = await resolveRemoteNote('b.test', note.id, carol);
+				await bob.client.request('notes/delete', { noteId: note.id });
+				await sleep();
+
+				await rejects(
+					async () => await carol.client.request('notes/show', { noteId: noteInA.id }),
+					(err: any) => {
+						strictEqual(err.code, 'NO_SUCH_NOTE');
+						return true;
+					},
+				);
+			});
+		});
+
+		describe('Deletion of remote user\'s note for moderation', () => {
+			let note: Misskey.entities.Note;
+
+			test('Alice post is deleted in B', async () => {
+				note = (await alice.client.request('notes/create', { text: 'Hello' })).createdNote;
+				const noteInB = await resolveRemoteNote('a.test', note.id, bob);
+				const bMod = await createModerator('b.test');
+				await bMod.client.request('notes/delete', { noteId: noteInB.id });
+				await rejects(
+					async () => await bob.client.request('notes/show', { noteId: noteInB.id }),
+					(err: any) => {
+						strictEqual(err.code, 'NO_SUCH_NOTE');
+						return true;
+					},
+				);
+			});
+
+			/**
+			 * FIXME: implement soft deletion as well as user?
+			 *        @see https://github.com/misskey-dev/misskey/issues/11437
+			 */
+			test.failing('Not found even if resolve again', async () => {
+				const noteInB = await resolveRemoteNote('a.test', note.id, bob);
+				await rejects(
+					async () => await bob.client.request('notes/show', { noteId: noteInB.id }),
+					(err: any) => {
+						strictEqual(err.code, 'NO_SUCH_NOTE');
+						return true;
+					},
+				);
+			});
+		});
+	});
+
+	describe('Reaction', () => {
+		describe('Consistency', () => {
+			test('Unicode reaction', async () => {
+				const note = (await alice.client.request('notes/create', { text: 'a' })).createdNote;
+				const resolvedNote = await resolveRemoteNote('a.test', note.id, bob);
+				const reaction = '😅';
+				await bob.client.request('notes/reactions/create', { noteId: resolvedNote.id, reaction });
+				await sleep();
+
+				const reactions = await alice.client.request('notes/reactions', { noteId: note.id });
+				strictEqual(reactions.length, 1);
+				strictEqual(reactions[0].type, reaction);
+				strictEqual(reactions[0].user.id, bobInA.id);
+			});
+
+			test('Custom emoji reaction', async () => {
+				const note = (await alice.client.request('notes/create', { text: 'a' })).createdNote;
+				const resolvedNote = await resolveRemoteNote('a.test', note.id, bob);
+				const emoji = await addCustomEmoji('b.test');
+				await bob.client.request('notes/reactions/create', { noteId: resolvedNote.id, reaction: `:${emoji.name}:` });
+				await sleep();
+
+				const reactions = await alice.client.request('notes/reactions', { noteId: note.id });
+				strictEqual(reactions.length, 1);
+				strictEqual(reactions[0].type, `:${emoji.name}@b.test:`);
+				strictEqual(reactions[0].user.id, bobInA.id);
+			});
+		});
+
+		describe('Acceptance', () => {
+			test('Even if likeOnly, remote users can react with custom emoji, but it is converted to like', async () => {
+				const note = (await alice.client.request('notes/create', { text: 'a', reactionAcceptance: 'likeOnly' })).createdNote;
+				const noteInB = await resolveRemoteNote('a.test', note.id, bob);
+				const emoji = await addCustomEmoji('b.test');
+				await bob.client.request('notes/reactions/create', { noteId: noteInB.id, reaction: `:${emoji.name}:` });
+				await sleep();
+
+				const reactions = await alice.client.request('notes/reactions', { noteId: note.id });
+				strictEqual(reactions.length, 1);
+				strictEqual(reactions[0].type, '❤');
+			});
+
+			/**
+			 * TODO: this may be unexpected behavior?
+			 *       @see https://github.com/misskey-dev/misskey/issues/12409
+			 */
+			test('Even if nonSensitiveOnly, remote users can react with sensitive emoji, and it is not converted', async () => {
+				const note = (await alice.client.request('notes/create', { text: 'a', reactionAcceptance: 'nonSensitiveOnly' })).createdNote;
+				const noteInB = await resolveRemoteNote('a.test', note.id, bob);
+				const emoji = await addCustomEmoji('b.test', { isSensitive: true });
+				await bob.client.request('notes/reactions/create', { noteId: noteInB.id, reaction: `:${emoji.name}:` });
+				await sleep();
+
+				const reactions = await alice.client.request('notes/reactions', { noteId: note.id });
+				strictEqual(reactions.length, 1);
+				strictEqual(reactions[0].type, `:${emoji.name}@b.test:`);
+			});
+		});
+	});
+
+	describe('Poll', () => {
+		describe('Any remote user\'s vote is delivered to the author', () => {
+			let carol: LoginUser;
+
+			beforeAll(async () => {
+				carol = await createAccount('a.test');
+			});
+
+			test('Bob creates poll and receives a vote from Carol', async () => {
+				const note = (await bob.client.request('notes/create', { poll: { choices: ['inu', 'neko'] } })).createdNote;
+				const noteInA = await resolveRemoteNote('b.test', note.id, carol);
+				await carol.client.request('notes/polls/vote', { noteId: noteInA.id, choice: 0 });
+				await sleep();
+
+				const noteAfterVote = await bob.client.request('notes/show', { noteId: note.id });
+				assert(noteAfterVote.poll != null);
+				strictEqual(noteAfterVote.poll.choices[0].votes, 1);
+				strictEqual(noteAfterVote.poll.choices[1].votes, 0);
+			});
+		});
+
+		describe('Local user\'s vote is delivered to the author\'s remote followers', () => {
+			let bobRemoteFollower: LoginUser, localVoter: LoginUser;
+
+			beforeAll(async () => {
+				[
+					bobRemoteFollower,
+					localVoter,
+				] = await Promise.all([
+					createAccount('a.test'),
+					createAccount('b.test'),
+				]);
+
+				await bobRemoteFollower.client.request('following/create', { userId: bobInA.id });
+				await sleep();
+			});
+
+			test('A vote in Bob\'s server is delivered to Bob\'s remote followers', async () => {
+				const note = (await bob.client.request('notes/create', { poll: { choices: ['inu', 'neko'] } })).createdNote;
+				// NOTE: resolve before voting
+				const noteInA = await resolveRemoteNote('b.test', note.id, bobRemoteFollower);
+				await localVoter.client.request('notes/polls/vote', { noteId: note.id, choice: 0 });
+				await sleep();
+
+				const noteAfterVote = await bobRemoteFollower.client.request('notes/show', { noteId: noteInA.id });
+				assert(noteAfterVote.poll != null);
+				strictEqual(noteAfterVote.poll.choices[0].votes, 1);
+				strictEqual(noteAfterVote.poll.choices[1].votes, 0);
+			});
+		});
+	});
+});
diff --git a/packages/backend/test-federation/test/notification.test.ts b/packages/backend/test-federation/test/notification.test.ts
new file mode 100644
index 0000000000..6d55353653
--- /dev/null
+++ b/packages/backend/test-federation/test/notification.test.ts
@@ -0,0 +1,107 @@
+import * as Misskey from 'misskey-js';
+import { assertNotificationReceived, createAccount, type LoginUser, resolveRemoteNote, resolveRemoteUser, sleep } from './utils.js';
+
+describe('Notification', () => {
+	let alice: LoginUser, bob: LoginUser;
+	let bobInA: Misskey.entities.UserDetailedNotMe, aliceInB: Misskey.entities.UserDetailedNotMe;
+
+	beforeAll(async () => {
+		[alice, bob] = await Promise.all([
+			createAccount('a.test'),
+			createAccount('b.test'),
+		]);
+
+		[bobInA, aliceInB] = await Promise.all([
+			resolveRemoteUser('b.test', bob.id, alice),
+			resolveRemoteUser('a.test', alice.id, bob),
+		]);
+	});
+
+	describe('Follow', () => {
+		test('Get notification when follow', async () => {
+			await assertNotificationReceived(
+				'b.test', bob,
+				async () => await bob.client.request('following/create', { userId: aliceInB.id }),
+				notification => notification.type === 'followRequestAccepted' && notification.userId === aliceInB.id,
+				true,
+			);
+
+			await bob.client.request('following/delete', { userId: aliceInB.id });
+			await sleep();
+		});
+
+		test('Get notification when get followed', async () => {
+			await assertNotificationReceived(
+				'a.test', alice,
+				async () => await bob.client.request('following/create', { userId: aliceInB.id }),
+				notification => notification.type === 'follow' && notification.userId === bobInA.id,
+				true,
+			);
+		});
+
+		afterAll(async () => await bob.client.request('following/delete', { userId: aliceInB.id }));
+	});
+
+	describe('Note', () => {
+		test('Get notification when get a reaction', async () => {
+			const note = (await alice.client.request('notes/create', { text: 'a' })).createdNote;
+			const noteInB = await resolveRemoteNote('a.test', note.id, bob);
+			const reaction = '😅';
+			await assertNotificationReceived(
+				'a.test', alice,
+				async () => await bob.client.request('notes/reactions/create', { noteId: noteInB.id, reaction }),
+				notification =>
+					notification.type === 'reaction' && notification.note.id === note.id && notification.userId === bobInA.id && notification.reaction === reaction,
+				true,
+			);
+		});
+
+		test('Get notification when replied', async () => {
+			const note = (await alice.client.request('notes/create', { text: 'a' })).createdNote;
+			const noteInB = await resolveRemoteNote('a.test', note.id, bob);
+			const text = crypto.randomUUID();
+			await assertNotificationReceived(
+				'a.test', alice,
+				async () => await bob.client.request('notes/create', { text, replyId: noteInB.id }),
+				notification =>
+					notification.type === 'reply' && notification.note.reply!.id === note.id && notification.userId === bobInA.id && notification.note.text === text,
+				true,
+			);
+		});
+
+		test('Get notification when renoted', async () => {
+			const note = (await alice.client.request('notes/create', { text: 'a' })).createdNote;
+			const noteInB = await resolveRemoteNote('a.test', note.id, bob);
+			await assertNotificationReceived(
+				'a.test', alice,
+				async () => await bob.client.request('notes/create', { renoteId: noteInB.id }),
+				notification =>
+					notification.type === 'renote' && notification.note.renote!.id === note.id && notification.userId === bobInA.id,
+				true,
+			);
+		});
+
+		test('Get notification when quoted', async () => {
+			const note = (await alice.client.request('notes/create', { text: 'a' })).createdNote;
+			const noteInB = await resolveRemoteNote('a.test', note.id, bob);
+			const text = crypto.randomUUID();
+			await assertNotificationReceived(
+				'a.test', alice,
+				async () => await bob.client.request('notes/create', { text, renoteId: noteInB.id }),
+				notification =>
+					notification.type === 'quote' && notification.note.renote!.id === note.id && notification.userId === bobInA.id && notification.note.text === text,
+				true,
+			);
+		});
+
+		test('Get notification when mentioned', async () => {
+			const text = `@${alice.username}@a.test`;
+			await assertNotificationReceived(
+				'a.test', alice,
+				async () => await bob.client.request('notes/create', { text }),
+				notification => notification.type === 'mention' && notification.userId === bobInA.id && notification.note.text === text,
+				true,
+			);
+		});
+	});
+});
diff --git a/packages/backend/test-federation/test/timeline.test.ts b/packages/backend/test-federation/test/timeline.test.ts
new file mode 100644
index 0000000000..2250bf4a42
--- /dev/null
+++ b/packages/backend/test-federation/test/timeline.test.ts
@@ -0,0 +1,328 @@
+import { strictEqual } from 'assert';
+import * as Misskey from 'misskey-js';
+import { createAccount, fetchAdmin, isNoteUpdatedEventFired, isFired, type LoginUser, type Request, resolveRemoteUser, sleep, createRole } from './utils.js';
+
+const bAdmin = await fetchAdmin('b.test');
+
+describe('Timeline', () => {
+	let alice: LoginUser, bob: LoginUser;
+	let bobInA: Misskey.entities.UserDetailedNotMe, aliceInB: Misskey.entities.UserDetailedNotMe;
+
+	beforeAll(async () => {
+		[alice, bob] = await Promise.all([
+			createAccount('a.test'),
+			createAccount('b.test'),
+		]);
+
+		[bobInA, aliceInB] = await Promise.all([
+			resolveRemoteUser('b.test', bob.id, alice),
+			resolveRemoteUser('a.test', alice.id, bob),
+		]);
+
+		await bob.client.request('following/create', { userId: aliceInB.id });
+		await sleep();
+	});
+
+	type TimelineChannel = keyof Misskey.Channels & (`${string}Timeline` | 'antenna' | 'userList' | 'hashtag');
+	type TimelineEndpoint = keyof Misskey.Endpoints & (`${string}timeline` | 'antennas/notes' | 'roles/notes' | 'notes/search-by-tag');
+	const timelineMap = new Map<TimelineChannel, TimelineEndpoint>([
+		['antenna', 'antennas/notes'],
+		['globalTimeline', 'notes/global-timeline'],
+		['homeTimeline', 'notes/timeline'],
+		['hybridTimeline', 'notes/hybrid-timeline'],
+		['localTimeline', 'notes/local-timeline'],
+		['roleTimeline', 'roles/notes'],
+		['hashtag', 'notes/search-by-tag'],
+		['userList', 'notes/user-list-timeline'],
+	]);
+
+	async function postAndCheckReception<C extends TimelineChannel>(
+		timelineChannel: C,
+		expect: boolean,
+		noteParams: Misskey.entities.NotesCreateRequest = {},
+		channelParams: Misskey.Channels[C]['params'] = {},
+	) {
+		let note: Misskey.entities.Note | undefined;
+		const text = noteParams.text ?? crypto.randomUUID();
+		const streamingFired = await isFired(
+			'b.test', bob, timelineChannel,
+			async () => {
+				note = (await alice.client.request('notes/create', { text, ...noteParams })).createdNote;
+			},
+			'note', msg => msg.text === text,
+			channelParams,
+		);
+		strictEqual(streamingFired, expect);
+
+		const endpoint = timelineMap.get(timelineChannel)!;
+		const params: Misskey.Endpoints[typeof endpoint]['req'] =
+			endpoint === 'antennas/notes' ? { antennaId: (channelParams as Misskey.Channels['antenna']['params']).antennaId } :
+			endpoint === 'notes/user-list-timeline' ? { listId: (channelParams as Misskey.Channels['userList']['params']).listId } :
+			endpoint === 'notes/search-by-tag' ? { query: (channelParams as Misskey.Channels['hashtag']['params']).q } :
+			endpoint === 'roles/notes' ? { roleId: (channelParams as Misskey.Channels['roleTimeline']['params']).roleId } :
+			{};
+
+		await sleep();
+		const notes = await (bob.client.request as Request)(endpoint, params);
+		const noteInB = notes.filter(({ uri }) => uri === `https://a.test/notes/${note!.id}`).pop();
+		const endpointFired = noteInB != null;
+		strictEqual(endpointFired, expect);
+
+		// Let's check Delete reception
+		if (expect) {
+			const streamingFired = await isNoteUpdatedEventFired(
+				'b.test', bob, noteInB!.id,
+				async () => await alice.client.request('notes/delete', { noteId: note!.id }),
+				msg => msg.type === 'deleted' && msg.id === noteInB!.id,
+			);
+			strictEqual(streamingFired, true);
+
+			await sleep();
+			const notes = await (bob.client.request as Request)(endpoint, params);
+			const endpointFired = notes.every(({ uri }) => uri !== `https://a.test/notes/${note!.id}`);
+			strictEqual(endpointFired, true);
+		}
+	}
+
+	describe('homeTimeline', () => {
+		// NOTE: narrowing scope intentionally to prevent mistakes by copy-and-paste
+		const homeTimeline = 'homeTimeline';
+
+		describe('Check reception of remote followee\'s Note', () => {
+			test('Receive remote followee\'s Note', async () => {
+				await postAndCheckReception(homeTimeline, true);
+			});
+
+			test('Receive remote followee\'s home-only Note', async () => {
+				await postAndCheckReception(homeTimeline, true, { visibility: 'home' });
+			});
+
+			test('Receive remote followee\'s followers-only Note', async () => {
+				await postAndCheckReception(homeTimeline, true, { visibility: 'followers' });
+			});
+
+			test('Receive remote followee\'s visible specified-only Note', async () => {
+				await postAndCheckReception(homeTimeline, true, { visibility: 'specified', visibleUserIds: [bobInA.id] });
+			});
+
+			test('Don\'t receive remote followee\'s localOnly Note', async () => {
+				await postAndCheckReception(homeTimeline, false, { localOnly: true });
+			});
+
+			test('Don\'t receive remote followee\'s invisible specified-only Note', async () => {
+				await postAndCheckReception(homeTimeline, false, { visibility: 'specified' });
+			});
+
+			/**
+			 * FIXME: can receive this
+			 * @see https://github.com/misskey-dev/misskey/issues/14083
+			 */
+			test.failing('Don\'t receive remote followee\'s invisible and mentioned specified-only Note', async () => {
+				await postAndCheckReception(homeTimeline, false, { text: `@${bob.username}@b.test Hello`, visibility: 'specified' });
+			});
+
+			/**
+			 * FIXME: cannot receive this
+			 * @see https://github.com/misskey-dev/misskey/issues/14084
+			 */
+			test.failing('Receive remote followee\'s visible specified-only reply to invisible specified-only Note', async () => {
+				const note = (await alice.client.request('notes/create', { text: 'a', visibility: 'specified' })).createdNote;
+				await postAndCheckReception(homeTimeline, true, { replyId: note.id, visibility: 'specified', visibleUserIds: [bobInA.id] });
+			});
+		});
+	});
+
+	describe('localTimeline', () => {
+		const localTimeline = 'localTimeline';
+
+		describe('Check reception of remote followee\'s Note', () => {
+			test('Don\'t receive remote followee\'s Note', async () => {
+				await postAndCheckReception(localTimeline, false);
+			});
+		});
+	});
+
+	describe('hybridTimeline', () => {
+		const hybridTimeline = 'hybridTimeline';
+
+		describe('Check reception of remote followee\'s Note', () => {
+			test('Receive remote followee\'s Note', async () => {
+				await postAndCheckReception(hybridTimeline, true);
+			});
+
+			test('Receive remote followee\'s home-only Note', async () => {
+				await postAndCheckReception(hybridTimeline, true, { visibility: 'home' });
+			});
+
+			test('Receive remote followee\'s followers-only Note', async () => {
+				await postAndCheckReception(hybridTimeline, true, { visibility: 'followers' });
+			});
+
+			test('Receive remote followee\'s visible specified-only Note', async () => {
+				await postAndCheckReception(hybridTimeline, true, { visibility: 'specified', visibleUserIds: [bobInA.id] });
+			});
+		});
+	});
+
+	describe('globalTimeline', () => {
+		const globalTimeline = 'globalTimeline';
+
+		describe('Check reception of remote followee\'s Note', () => {
+			test('Receive remote followee\'s Note', async () => {
+				await postAndCheckReception(globalTimeline, true);
+			});
+
+			test('Don\'t receive remote followee\'s home-only Note', async () => {
+				await postAndCheckReception(globalTimeline, false, { visibility: 'home' });
+			});
+
+			test('Don\'t receive remote followee\'s followers-only Note', async () => {
+				await postAndCheckReception(globalTimeline, false, { visibility: 'followers' });
+			});
+
+			test('Don\'t receive remote followee\'s visible specified-only Note', async () => {
+				await postAndCheckReception(globalTimeline, false, { visibility: 'specified', visibleUserIds: [bobInA.id] });
+			});
+		});
+	});
+
+	describe('userList', () => {
+		const userList = 'userList';
+
+		let list: Misskey.entities.UserList;
+
+		beforeAll(async () => {
+			list = await bob.client.request('users/lists/create', { name: 'Bob\'s List' });
+			await bob.client.request('users/lists/push', { listId: list.id, userId: aliceInB.id });
+			await sleep();
+		});
+
+		describe('Check reception of remote followee\'s Note', () => {
+			test('Receive remote followee\'s Note', async () => {
+				await postAndCheckReception(userList, true, {}, { listId: list.id });
+			});
+
+			test('Receive remote followee\'s home-only Note', async () => {
+				await postAndCheckReception(userList, true, { visibility: 'home' }, { listId: list.id });
+			});
+
+			test('Receive remote followee\'s followers-only Note', async () => {
+				await postAndCheckReception(userList, true, { visibility: 'followers' }, { listId: list.id });
+			});
+
+			test('Receive remote followee\'s visible specified-only Note', async () => {
+				await postAndCheckReception(userList, true, { visibility: 'specified', visibleUserIds: [bobInA.id] }, { listId: list.id });
+			});
+		});
+	});
+
+	describe('hashtag', () => {
+		const hashtag = 'hashtag';
+
+		describe('Check reception of remote followee\'s Note', () => {
+			test('Receive remote followee\'s Note', async () => {
+				const tag = crypto.randomUUID();
+				await postAndCheckReception(hashtag, true, { text: `#${tag}` }, { q: [[tag]] });
+			});
+
+			test('Receive remote followee\'s home-only Note', async () => {
+				const tag = crypto.randomUUID();
+				await postAndCheckReception(hashtag, true, { text: `#${tag}`, visibility: 'home' }, { q: [[tag]] });
+			});
+
+			test('Receive remote followee\'s followers-only Note', async () => {
+				const tag = crypto.randomUUID();
+				await postAndCheckReception(hashtag, true, { text: `#${tag}`, visibility: 'followers' }, { q: [[tag]] });
+			});
+
+			test('Receive remote followee\'s visible specified-only Note', async () => {
+				const tag = crypto.randomUUID();
+				await postAndCheckReception(hashtag, true, { text: `#${tag}`, visibility: 'specified', visibleUserIds: [bobInA.id] }, { q: [[tag]] });
+			});
+		});
+	});
+
+	describe('roleTimeline', () => {
+		const roleTimeline = 'roleTimeline';
+
+		let role: Misskey.entities.Role;
+
+		beforeAll(async () => {
+			role = await createRole('b.test', {
+				name: 'Remote Users',
+				description: 'Remote users are assigned to this role.',
+				condFormula: {
+					/** TODO: @see https://github.com/misskey-dev/misskey/issues/14169 */
+					type: 'isRemote' as never,
+				},
+			});
+			await sleep();
+		});
+
+		describe('Check reception of remote followee\'s Note', () => {
+			test('Receive remote followee\'s Note', async () => {
+				await postAndCheckReception(roleTimeline, true, {}, { roleId: role.id });
+			});
+
+			test('Don\'t receive remote followee\'s home-only Note', async () => {
+				await postAndCheckReception(roleTimeline, false, { visibility: 'home' }, { roleId: role.id });
+			});
+
+			test('Don\'t receive remote followee\'s followers-only Note', async () => {
+				await postAndCheckReception(roleTimeline, false, { visibility: 'followers' }, { roleId: role.id });
+			});
+
+			test('Don\'t receive remote followee\'s visible specified-only Note', async () => {
+				await postAndCheckReception(roleTimeline, false, { visibility: 'specified', visibleUserIds: [bobInA.id] }, { roleId: role.id });
+			});
+		});
+
+		afterAll(async () => {
+			await bAdmin.client.request('admin/roles/delete', { roleId: role.id });
+		});
+	});
+
+	// TODO: Cannot test
+	describe.skip('antenna', () => {
+		const antenna = 'antenna';
+
+		let bobAntenna: Misskey.entities.Antenna;
+
+		beforeAll(async () => {
+			bobAntenna = await bob.client.request('antennas/create', {
+				name: 'Bob\'s Egosurfing Antenna',
+				src: 'all',
+				keywords: [['Bob']],
+				excludeKeywords: [],
+				users: [],
+				caseSensitive: false,
+				localOnly: false,
+				withReplies: true,
+				withFile: true,
+			});
+			await sleep();
+		});
+
+		describe('Check reception of remote followee\'s Note', () => {
+			test('Receive remote followee\'s Note', async () => {
+				await postAndCheckReception(antenna, true, { text: 'I love Bob (1)' }, { antennaId: bobAntenna.id });
+			});
+
+			test('Don\'t receive remote followee\'s home-only Note', async () => {
+				await postAndCheckReception(antenna, false, { text: 'I love Bob (2)', visibility: 'home' }, { antennaId: bobAntenna.id });
+			});
+
+			test('Don\'t receive remote followee\'s followers-only Note', async () => {
+				await postAndCheckReception(antenna, false, { text: 'I love Bob (3)', visibility: 'followers' }, { antennaId: bobAntenna.id });
+			});
+
+			test('Don\'t receive remote followee\'s visible specified-only Note', async () => {
+				await postAndCheckReception(antenna, false, { text: 'I love Bob (4)', visibility: 'specified', visibleUserIds: [bobInA.id] }, { antennaId: bobAntenna.id });
+			});
+		});
+
+		afterAll(async () => {
+			await bob.client.request('antennas/delete', { antennaId: bobAntenna.id });
+		});
+	});
+});
diff --git a/packages/backend/test-federation/test/user.test.ts b/packages/backend/test-federation/test/user.test.ts
new file mode 100644
index 0000000000..76605e61d4
--- /dev/null
+++ b/packages/backend/test-federation/test/user.test.ts
@@ -0,0 +1,560 @@
+import assert, { rejects, strictEqual } from 'node:assert';
+import * as Misskey from 'misskey-js';
+import { createAccount, deepStrictEqualWithExcludedFields, fetchAdmin, type LoginUser, resolveRemoteNote, resolveRemoteUser, sleep } from './utils.js';
+
+const [aAdmin, bAdmin] = await Promise.all([
+	fetchAdmin('a.test'),
+	fetchAdmin('b.test'),
+]);
+
+describe('User', () => {
+	describe('Profile', () => {
+		describe('Consistency of profile', () => {
+			let alice: LoginUser;
+			let aliceWatcher: LoginUser;
+			let aliceWatcherInB: LoginUser;
+
+			beforeAll(async () => {
+				alice = await createAccount('a.test');
+				[
+					aliceWatcher,
+					aliceWatcherInB,
+				] = await Promise.all([
+					createAccount('a.test'),
+					createAccount('b.test'),
+				]);
+			});
+
+			test('Check consistency', async () => {
+				const aliceInA = await aliceWatcher.client.request('users/show', { userId: alice.id });
+				const resolved = await resolveRemoteUser('a.test', aliceInA.id, aliceWatcherInB);
+				const aliceInB = await aliceWatcherInB.client.request('users/show', { userId: resolved.id });
+
+				// console.log(`a.test: ${JSON.stringify(aliceInA, null, '\t')}`);
+				// console.log(`b.test: ${JSON.stringify(aliceInB, null, '\t')}`);
+
+				deepStrictEqualWithExcludedFields(aliceInA, aliceInB, [
+					'id',
+					'host',
+					'avatarUrl',
+					'instance',
+					'badgeRoles',
+					'url',
+					'uri',
+					'createdAt',
+					'lastFetchedAt',
+					'publicReactions',
+				]);
+			});
+		});
+
+		describe('ffVisibility is federated', () => {
+			let alice: LoginUser, bob: LoginUser;
+			let bobInA: Misskey.entities.UserDetailedNotMe, aliceInB: Misskey.entities.UserDetailedNotMe;
+
+			beforeAll(async () => {
+				[alice, bob] = await Promise.all([
+					createAccount('a.test'),
+					createAccount('b.test'),
+				]);
+
+				[bobInA, aliceInB] = await Promise.all([
+					resolveRemoteUser('b.test', bob.id, alice),
+					resolveRemoteUser('a.test', alice.id, bob),
+				]);
+
+				// NOTE: follow each other
+				await Promise.all([
+					alice.client.request('following/create', { userId: bobInA.id }),
+					bob.client.request('following/create', { userId: aliceInB.id }),
+				]);
+				await sleep();
+			});
+
+			test('Visibility set public by default', async () => {
+				for (const user of await Promise.all([
+					alice.client.request('users/show', { userId: bobInA.id }),
+					bob.client.request('users/show', { userId: aliceInB.id }),
+				])) {
+					strictEqual(user.followersVisibility, 'public');
+					strictEqual(user.followingVisibility, 'public');
+				}
+			});
+
+			/** FIXME: not working */
+			test.skip('Setting private for followersVisibility is federated', async () => {
+				await Promise.all([
+					alice.client.request('i/update', { followersVisibility: 'private' }),
+					bob.client.request('i/update', { followersVisibility: 'private' }),
+				]);
+				await sleep();
+
+				for (const user of await Promise.all([
+					alice.client.request('users/show', { userId: bobInA.id }),
+					bob.client.request('users/show', { userId: aliceInB.id }),
+				])) {
+					strictEqual(user.followersVisibility, 'private');
+					strictEqual(user.followingVisibility, 'public');
+				}
+			});
+
+			test.skip('Setting private for followingVisibility is federated', async () => {
+				await Promise.all([
+					alice.client.request('i/update', { followingVisibility: 'private' }),
+					bob.client.request('i/update', { followingVisibility: 'private' }),
+				]);
+				await sleep();
+
+				for (const user of await Promise.all([
+					alice.client.request('users/show', { userId: bobInA.id }),
+					bob.client.request('users/show', { userId: aliceInB.id }),
+				])) {
+					strictEqual(user.followersVisibility, 'private');
+					strictEqual(user.followingVisibility, 'private');
+				}
+			});
+		});
+
+		describe('isCat is federated', () => {
+			let alice: LoginUser, bob: LoginUser;
+			let bobInA: Misskey.entities.UserDetailedNotMe, aliceInB: Misskey.entities.UserDetailedNotMe;
+
+			beforeAll(async () => {
+				[alice, bob] = await Promise.all([
+					createAccount('a.test'),
+					createAccount('b.test'),
+				]);
+
+				[bobInA, aliceInB] = await Promise.all([
+					resolveRemoteUser('b.test', bob.id, alice),
+					resolveRemoteUser('a.test', alice.id, bob),
+				]);
+			});
+
+			test('Not isCat for default', () => {
+				strictEqual(aliceInB.isCat, false);
+			});
+
+			test('Becoming a cat is sent to their followers', async () => {
+				await bob.client.request('following/create', { userId: aliceInB.id });
+				await sleep();
+
+				await alice.client.request('i/update', { isCat: true });
+				await sleep();
+
+				const res = await bob.client.request('users/show', { userId: aliceInB.id });
+				strictEqual(res.isCat, true);
+			});
+		});
+
+		describe('Pinning Notes', () => {
+			let alice: LoginUser, bob: LoginUser;
+			let aliceInB: Misskey.entities.UserDetailedNotMe;
+
+			beforeAll(async () => {
+				[alice, bob] = await Promise.all([
+					createAccount('a.test'),
+					createAccount('b.test'),
+				]);
+				aliceInB = await resolveRemoteUser('a.test', alice.id, bob);
+
+				await bob.client.request('following/create', { userId: aliceInB.id });
+			});
+
+			test('Pinning localOnly Note is not delivered', async () => {
+				const note = (await alice.client.request('notes/create', { text: 'a', localOnly: true })).createdNote;
+				await alice.client.request('i/pin', { noteId: note.id });
+				await sleep();
+
+				const _aliceInB = await bob.client.request('users/show', { userId: aliceInB.id });
+				strictEqual(_aliceInB.pinnedNoteIds.length, 0);
+			});
+
+			test('Pinning followers-only Note is not delivered', async () => {
+				const note = (await alice.client.request('notes/create', { text: 'a', visibility: 'followers' })).createdNote;
+				await alice.client.request('i/pin', { noteId: note.id });
+				await sleep();
+
+				const _aliceInB = await bob.client.request('users/show', { userId: aliceInB.id });
+				strictEqual(_aliceInB.pinnedNoteIds.length, 0);
+			});
+
+			let pinnedNote: Misskey.entities.Note;
+
+			test('Pinning normal Note is delivered', async () => {
+				pinnedNote = (await alice.client.request('notes/create', { text: 'a' })).createdNote;
+				await alice.client.request('i/pin', { noteId: pinnedNote.id });
+				await sleep();
+
+				const _aliceInB = await bob.client.request('users/show', { userId: aliceInB.id });
+				strictEqual(_aliceInB.pinnedNoteIds.length, 1);
+				const pinnedNoteInB = await resolveRemoteNote('a.test', pinnedNote.id, bob);
+				strictEqual(_aliceInB.pinnedNotes[0].id, pinnedNoteInB.id);
+			});
+
+			test('Unpinning normal Note is delivered', async () => {
+				await alice.client.request('i/unpin', { noteId: pinnedNote.id });
+				await sleep();
+
+				const _aliceInB = await bob.client.request('users/show', { userId: aliceInB.id });
+				strictEqual(_aliceInB.pinnedNoteIds.length, 0);
+			});
+		});
+	});
+
+	describe('Follow / Unfollow', () => {
+		let alice: LoginUser, bob: LoginUser;
+		let bobInA: Misskey.entities.UserDetailedNotMe, aliceInB: Misskey.entities.UserDetailedNotMe;
+
+		beforeAll(async () => {
+			[alice, bob] = await Promise.all([
+				createAccount('a.test'),
+				createAccount('b.test'),
+			]);
+
+			[bobInA, aliceInB] = await Promise.all([
+				resolveRemoteUser('b.test', bob.id, alice),
+				resolveRemoteUser('a.test', alice.id, bob),
+			]);
+		});
+
+		describe('Follow a.test ==> b.test', () => {
+			beforeAll(async () => {
+				await alice.client.request('following/create', { userId: bobInA.id });
+
+				await sleep();
+			});
+
+			test('Check consistency with `users/following` and `users/followers` endpoints', async () => {
+				await Promise.all([
+					strictEqual(
+						(await alice.client.request('users/following', { userId: alice.id }))
+							.some(v => v.followeeId === bobInA.id),
+						true,
+					),
+					strictEqual(
+						(await bob.client.request('users/followers', { userId: bob.id }))
+							.some(v => v.followerId === aliceInB.id),
+						true,
+					),
+				]);
+			});
+		});
+
+		describe('Unfollow a.test ==> b.test', () => {
+			beforeAll(async () => {
+				await alice.client.request('following/delete', { userId: bobInA.id });
+
+				await sleep();
+			});
+
+			test('Check consistency with `users/following` and `users/followers` endpoints', async () => {
+				await Promise.all([
+					strictEqual(
+						(await alice.client.request('users/following', { userId: alice.id }))
+							.some(v => v.followeeId === bobInA.id),
+						false,
+					),
+					strictEqual(
+						(await bob.client.request('users/followers', { userId: bob.id }))
+							.some(v => v.followerId === aliceInB.id),
+						false,
+					),
+				]);
+			});
+		});
+	});
+
+	describe('Follow requests', () => {
+		let alice: LoginUser, bob: LoginUser;
+		let bobInA: Misskey.entities.UserDetailedNotMe, aliceInB: Misskey.entities.UserDetailedNotMe;
+
+		beforeAll(async () => {
+			[alice, bob] = await Promise.all([
+				createAccount('a.test'),
+				createAccount('b.test'),
+			]);
+
+			[bobInA, aliceInB] = await Promise.all([
+				resolveRemoteUser('b.test', bob.id, alice),
+				resolveRemoteUser('a.test', alice.id, bob),
+			]);
+
+			await alice.client.request('i/update', { isLocked: true });
+		});
+
+		describe('Send follow request from Bob to Alice and cancel', () => {
+			describe('Bob sends follow request to Alice', () => {
+				beforeAll(async () => {
+					await bob.client.request('following/create', { userId: aliceInB.id });
+					await sleep();
+				});
+
+				test('Alice should have a request', async () => {
+					const requests = await alice.client.request('following/requests/list', {});
+					strictEqual(requests.length, 1);
+					strictEqual(requests[0].followee.id, alice.id);
+					strictEqual(requests[0].follower.id, bobInA.id);
+				});
+			});
+
+			describe('Alice cancels it', () => {
+				beforeAll(async () => {
+					await bob.client.request('following/requests/cancel', { userId: aliceInB.id });
+					await sleep();
+				});
+
+				test('Alice should have no requests', async () => {
+					const requests = await alice.client.request('following/requests/list', {});
+					strictEqual(requests.length, 0);
+				});
+			});
+		});
+
+		describe('Send follow request from Bob to Alice and reject', () => {
+			beforeAll(async () => {
+				await bob.client.request('following/create', { userId: aliceInB.id });
+				await sleep();
+
+				await alice.client.request('following/requests/reject', { userId: bobInA.id });
+				await sleep();
+			});
+
+			test('Bob should have no requests', async () => {
+				await rejects(
+					async () => await bob.client.request('following/requests/cancel', { userId: aliceInB.id }),
+					(err: any) => {
+						strictEqual(err.code, 'FOLLOW_REQUEST_NOT_FOUND');
+						return true;
+					},
+				);
+			});
+
+			test('Bob doesn\'t follow Alice', async () => {
+				const following = await bob.client.request('users/following', { userId: bob.id });
+				strictEqual(following.length, 0);
+			});
+		});
+
+		describe('Send follow request from Bob to Alice and accept', () => {
+			beforeAll(async () => {
+				await bob.client.request('following/create', { userId: aliceInB.id });
+				await sleep();
+
+				await alice.client.request('following/requests/accept', { userId: bobInA.id });
+				await sleep();
+			});
+
+			test('Bob follows Alice', async () => {
+				const following = await bob.client.request('users/following', { userId: bob.id });
+				strictEqual(following.length, 1);
+				strictEqual(following[0].followeeId, aliceInB.id);
+				strictEqual(following[0].followerId, bob.id);
+			});
+		});
+	});
+
+	describe('Deletion', () => {
+		describe('Check Delete consistency', () => {
+			let alice: LoginUser, bob: LoginUser;
+			let bobInA: Misskey.entities.UserDetailedNotMe, aliceInB: Misskey.entities.UserDetailedNotMe;
+
+			beforeAll(async () => {
+				[alice, bob] = await Promise.all([
+					createAccount('a.test'),
+					createAccount('b.test'),
+				]);
+
+				[bobInA, aliceInB] = await Promise.all([
+					resolveRemoteUser('b.test', bob.id, alice),
+					resolveRemoteUser('a.test', alice.id, bob),
+				]);
+			});
+
+			test('Bob follows Alice, and Alice deleted themself', async () => {
+				await bob.client.request('following/create', { userId: aliceInB.id });
+				await sleep();
+
+				const followers = await alice.client.request('users/followers', { userId: alice.id });
+				strictEqual(followers.length, 1); // followed by Bob
+
+				await alice.client.request('i/delete-account', { password: alice.password });
+				await sleep();
+
+				const following = await bob.client.request('users/following', { userId: bob.id });
+				strictEqual(following.length, 0); // no following relation
+
+				await rejects(
+					async () => await bob.client.request('following/create', { userId: aliceInB.id }),
+					(err: any) => {
+						strictEqual(err.code, 'NO_SUCH_USER');
+						return true;
+					},
+				);
+			});
+		});
+
+		describe('Deletion of remote user for moderation', () => {
+			let alice: LoginUser, bob: LoginUser;
+			let bobInA: Misskey.entities.UserDetailedNotMe, aliceInB: Misskey.entities.UserDetailedNotMe;
+
+			beforeAll(async () => {
+				[alice, bob] = await Promise.all([
+					createAccount('a.test'),
+					createAccount('b.test'),
+				]);
+
+				[bobInA, aliceInB] = await Promise.all([
+					resolveRemoteUser('b.test', bob.id, alice),
+					resolveRemoteUser('a.test', alice.id, bob),
+				]);
+			});
+
+			test('Bob follows Alice, then Alice gets deleted in B server', async () => {
+				await bob.client.request('following/create', { userId: aliceInB.id });
+				await sleep();
+
+				const followers = await alice.client.request('users/followers', { userId: alice.id });
+				strictEqual(followers.length, 1); // followed by Bob
+
+				await bAdmin.client.request('admin/delete-account', { userId: aliceInB.id });
+				await sleep();
+
+				/**
+				 * FIXME: remote account is not deleted!
+				 *        @see https://github.com/misskey-dev/misskey/issues/14728
+				 */
+				const deletedAlice = await bob.client.request('users/show', { userId: aliceInB.id });
+				assert(deletedAlice.id, aliceInB.id);
+
+				// TODO: why still following relation?
+				const following = await bob.client.request('users/following', { userId: bob.id });
+				strictEqual(following.length, 1);
+				await rejects(
+					async () => await bob.client.request('following/create', { userId: aliceInB.id }),
+					(err: any) => {
+						strictEqual(err.code, 'ALREADY_FOLLOWING');
+						return true;
+					},
+				);
+			});
+
+			test('Alice tries to follow Bob, but it is not processed', async () => {
+				await alice.client.request('following/create', { userId: bobInA.id });
+				await sleep();
+
+				const following = await alice.client.request('users/following', { userId: alice.id });
+				strictEqual(following.length, 0); // Not following Bob because B server doesn't return Accept
+
+				const followers = await bob.client.request('users/followers', { userId: bob.id });
+				strictEqual(followers.length, 0); // Alice's Follow is not processed
+			});
+		});
+	});
+
+	describe('Suspension', () => {
+		describe('Check suspend/unsuspend consistency', () => {
+			let alice: LoginUser, bob: LoginUser;
+			let bobInA: Misskey.entities.UserDetailedNotMe, aliceInB: Misskey.entities.UserDetailedNotMe;
+
+			beforeAll(async () => {
+				[alice, bob] = await Promise.all([
+					createAccount('a.test'),
+					createAccount('b.test'),
+				]);
+
+				[bobInA, aliceInB] = await Promise.all([
+					resolveRemoteUser('b.test', bob.id, alice),
+					resolveRemoteUser('a.test', alice.id, bob),
+				]);
+			});
+
+			test('Bob follows Alice, and Alice gets suspended, there is no following relation, and Bob fails to follow again', async () => {
+				await bob.client.request('following/create', { userId: aliceInB.id });
+				await sleep();
+
+				const followers = await alice.client.request('users/followers', { userId: alice.id });
+				strictEqual(followers.length, 1); // followed by Bob
+
+				await aAdmin.client.request('admin/suspend-user', { userId: alice.id });
+				await sleep();
+
+				const following = await bob.client.request('users/following', { userId: bob.id });
+				strictEqual(following.length, 0); // no following relation
+
+				await rejects(
+					async () => await bob.client.request('following/create', { userId: aliceInB.id }),
+					(err: any) => {
+						strictEqual(err.code, 'NO_SUCH_USER');
+						return true;
+					},
+				);
+			});
+
+			test('Alice gets unsuspended, Bob succeeds in following Alice', async () => {
+				await aAdmin.client.request('admin/unsuspend-user', { userId: alice.id });
+				await sleep();
+
+				const followers = await alice.client.request('users/followers', { userId: alice.id });
+				strictEqual(followers.length, 1); // FIXME: followers are not deleted??
+
+				/**
+				 * FIXME: still rejected!
+				 *        seems to can't process Undo Delete activity because it is not implemented
+				 *        related @see https://github.com/misskey-dev/misskey/issues/13273
+				 */
+				await rejects(
+					async () => await bob.client.request('following/create', { userId: aliceInB.id }),
+					(err: any) => {
+						strictEqual(err.code, 'NO_SUCH_USER');
+						return true;
+					},
+				);
+
+				// FIXME: resolving also fails
+				await rejects(
+					async () => await resolveRemoteUser('a.test', alice.id, bob),
+					(err: any) => {
+						strictEqual(err.code, 'INTERNAL_ERROR');
+						return true;
+					},
+				);
+			});
+
+			/**
+			 * instead of simple unsuspension, let's tell existence by following from Alice
+			 */
+			test('Alice can follow Bob', async () => {
+				await alice.client.request('following/create', { userId: bobInA.id });
+				await sleep();
+
+				const bobFollowers = await bob.client.request('users/followers', { userId: bob.id });
+				strictEqual(bobFollowers.length, 1); // followed by Alice
+				assert(bobFollowers[0].follower != null);
+				const renewedaliceInB = bobFollowers[0].follower;
+				assert(aliceInB.username === renewedaliceInB.username);
+				assert(aliceInB.host === renewedaliceInB.host);
+				assert(aliceInB.id !== renewedaliceInB.id); // TODO: Same username and host, but their ids are different! Is it OK?
+
+				const following = await bob.client.request('users/following', { userId: bob.id });
+				strictEqual(following.length, 0); // following are deleted
+
+				// Bob tries to follow Alice
+				await bob.client.request('following/create', { userId: renewedaliceInB.id });
+				await sleep();
+
+				const aliceFollowers = await alice.client.request('users/followers', { userId: alice.id });
+				strictEqual(aliceFollowers.length, 1);
+
+				// FIXME: but resolving still fails ...
+				await rejects(
+					async () => await resolveRemoteUser('a.test', alice.id, bob),
+					(err: any) => {
+						strictEqual(err.code, 'INTERNAL_ERROR');
+						return true;
+					},
+				);
+			});
+		});
+	});
+});
diff --git a/packages/backend/test-federation/test/utils.ts b/packages/backend/test-federation/test/utils.ts
new file mode 100644
index 0000000000..483bf4b254
--- /dev/null
+++ b/packages/backend/test-federation/test/utils.ts
@@ -0,0 +1,309 @@
+import { deepStrictEqual, strictEqual } from 'assert';
+import { readFile } from 'fs/promises';
+import { dirname, join } from 'path';
+import { fileURLToPath } from 'url';
+import * as Misskey from 'misskey-js';
+import { WebSocket } from 'ws';
+
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = dirname(__filename);
+
+export const ADMIN_PARAMS = { username: 'admin', password: 'admin' };
+const ADMIN_CACHE = new Map<Host, SigninResponse>();
+
+await Promise.all([
+	fetchAdmin('a.test'),
+	fetchAdmin('b.test'),
+]);
+
+type SigninResponse = Omit<Misskey.entities.SigninFlowResponse & { finished: true }, 'finished'>;
+
+export type LoginUser = SigninResponse & {
+	client: Misskey.api.APIClient;
+	username: string;
+	password: string;
+}
+
+/** used for avoiding overload and some endpoints */
+export type Request = <
+	E extends keyof Misskey.Endpoints,
+	P extends Misskey.Endpoints[E]['req'],
+>(
+	endpoint: E,
+	params: P,
+	credential?: string | null,
+) => Promise<Misskey.api.SwitchCaseResponseType<E, P>>;
+
+type Host = 'a.test' | 'b.test';
+
+export async function sleep(ms = 200): Promise<void> {
+	return new Promise(resolve => setTimeout(resolve, ms));
+}
+
+async function signin(
+	host: Host,
+	params: Misskey.entities.SigninFlowRequest,
+): Promise<SigninResponse> {
+	// wait for a second to prevent hit rate limit
+	await sleep(1000);
+
+	return await (new Misskey.api.APIClient({ origin: `https://${host}` }).request as Request)('signin-flow', params)
+		.then(res => {
+			strictEqual(res.finished, true);
+			if (params.username === ADMIN_PARAMS.username) ADMIN_CACHE.set(host, res);
+			return res;
+		})
+		.then(({ id, i }) => ({ id, i }))
+		.catch(async err => {
+			if (err.code === 'TOO_MANY_AUTHENTICATION_FAILURES') {
+				await sleep(Math.random() * 2000);
+				return await signin(host, params);
+			}
+			throw err;
+		});
+}
+
+async function createAdmin(host: Host): Promise<Misskey.entities.SignupResponse | undefined> {
+	const client = new Misskey.api.APIClient({ origin: `https://${host}` });
+	return await client.request('admin/accounts/create', ADMIN_PARAMS).then(res => {
+		ADMIN_CACHE.set(host, {
+			id: res.id,
+			// @ts-expect-error FIXME: openapi-typescript generates incorrect response type for this endpoint, so ignore this
+			i: res.token,
+		});
+		return res as Misskey.entities.SignupResponse;
+	}).then(async res => {
+		await client.request('admin/roles/update-default-policies', {
+			policies: {
+				/** TODO: @see https://github.com/misskey-dev/misskey/issues/14169 */
+				rateLimitFactor: 0 as never,
+			},
+		}, res.token);
+		return res;
+	}).catch(err => {
+		if (err.info.e.message === 'access denied') return undefined;
+		throw err;
+	});
+}
+
+export async function fetchAdmin(host: Host): Promise<LoginUser> {
+	const admin = ADMIN_CACHE.get(host) ?? await signin(host, ADMIN_PARAMS)
+		.catch(async err => {
+			if (err.id === '6cc579cc-885d-43d8-95c2-b8c7fc963280') {
+				await createAdmin(host);
+				return await signin(host, ADMIN_PARAMS);
+			}
+			throw err;
+		});
+
+	return {
+		...admin,
+		client: new Misskey.api.APIClient({ origin: `https://${host}`, credential: admin.i }),
+		...ADMIN_PARAMS,
+	};
+}
+
+export async function createAccount(host: Host): Promise<LoginUser> {
+	const username = crypto.randomUUID().replaceAll('-', '').substring(0, 20);
+	const password = crypto.randomUUID().replaceAll('-', '');
+	const admin = await fetchAdmin(host);
+	await admin.client.request('admin/accounts/create', { username, password });
+	const signinRes = await signin(host, { username, password });
+
+	return {
+		...signinRes,
+		client: new Misskey.api.APIClient({ origin: `https://${host}`, credential: signinRes.i }),
+		username,
+		password,
+	};
+}
+
+export async function createModerator(host: Host): Promise<LoginUser> {
+	const user = await createAccount(host);
+	const role = await createRole(host, {
+		name: 'Moderator',
+		isModerator: true,
+	});
+	const admin = await fetchAdmin(host);
+	await admin.client.request('admin/roles/assign', { roleId: role.id, userId: user.id });
+	return user;
+}
+
+export async function createRole(
+	host: Host,
+	params: Partial<Misskey.entities.AdminRolesCreateRequest> = {},
+): Promise<Misskey.entities.Role> {
+	const admin = await fetchAdmin(host);
+	return await admin.client.request('admin/roles/create', {
+		name: 'Some role',
+		description: 'Role for testing',
+		color: null,
+		iconUrl: null,
+		target: 'conditional',
+		condFormula: {},
+		isPublic: true,
+		isModerator: false,
+		isAdministrator: false,
+		isExplorable: true,
+		asBadge: false,
+		canEditMembersByModerator: false,
+		displayOrder: 0,
+		policies: {},
+		...params,
+	});
+}
+
+export async function resolveRemoteUser(
+	host: Host,
+	id: string,
+	from: LoginUser,
+): Promise<Misskey.entities.UserDetailedNotMe> {
+	const uri = `https://${host}/users/${id}`;
+	return await from.client.request('ap/show', { uri })
+		.then(res => {
+			strictEqual(res.type, 'User');
+			strictEqual(res.object.uri, uri);
+			return res.object;
+		});
+}
+
+export async function resolveRemoteNote(
+	host: Host,
+	id: string,
+	from: LoginUser,
+): Promise<Misskey.entities.Note> {
+	const uri = `https://${host}/notes/${id}`;
+	return await from.client.request('ap/show', { uri })
+		.then(res => {
+			strictEqual(res.type, 'Note');
+			strictEqual(res.object.uri, uri);
+			return res.object;
+		});
+}
+
+export async function uploadFile(
+	host: Host,
+	user: { i: string },
+	path = '../../test/resources/192.jpg',
+): Promise<Misskey.entities.DriveFile> {
+	const filename = path.split('/').pop() ?? 'untitled';
+	const blob = new Blob([await readFile(join(__dirname, path))]);
+
+	const body = new FormData();
+	body.append('i', user.i);
+	body.append('force', 'true');
+	body.append('file', blob);
+	body.append('name', filename);
+
+	return await fetch(`https://${host}/api/drive/files/create`, { method: 'POST', body })
+		.then(async res => await res.json());
+}
+
+export async function addCustomEmoji(
+	host: Host,
+	param?: Partial<Misskey.entities.AdminEmojiAddRequest>,
+	path?: string,
+): Promise<Misskey.entities.EmojiDetailed> {
+	const admin = await fetchAdmin(host);
+	const name = crypto.randomUUID().replaceAll('-', '');
+	const file = await uploadFile(host, admin, path);
+	return await admin.client.request('admin/emoji/add', { name, fileId: file.id, ...param });
+}
+
+export function deepStrictEqualWithExcludedFields<T>(actual: T, expected: T, excludedFields: (keyof T)[]) {
+	const _actual = structuredClone(actual);
+	const _expected = structuredClone(expected);
+	for (const obj of [_actual, _expected]) {
+		for (const field of excludedFields) {
+			delete obj[field];
+		}
+	}
+	deepStrictEqual(_actual, _expected);
+}
+
+export async function isFired<C extends keyof Misskey.Channels, T extends keyof Misskey.Channels[C]['events']>(
+	host: Host,
+	user: { i: string },
+	channel: C,
+	trigger: () => Promise<unknown>,
+	type: T,
+	// @ts-expect-error TODO: why getting error here?
+	cond: (msg: Parameters<Misskey.Channels[C]['events'][T]>[0]) => boolean,
+	params?: Misskey.Channels[C]['params'],
+): Promise<boolean> {
+	return new Promise<boolean>(async (resolve, reject) => {
+		// @ts-expect-error TODO: why?
+		const stream = new Misskey.Stream(`wss://${host}`, { token: user.i }, { WebSocket });
+		const connection = stream.useChannel(channel, params);
+		connection.on(type as any, ((msg: any) => {
+			if (cond(msg)) {
+				stream.close();
+				clearTimeout(timer);
+				resolve(true);
+			}
+		}) as any);
+
+		let timer: NodeJS.Timeout | undefined;
+
+		await trigger().then(() => {
+			timer = setTimeout(() => {
+				stream.close();
+				resolve(false);
+			}, 500);
+		}).catch(err => {
+			stream.close();
+			clearTimeout(timer);
+			reject(err);
+		});
+	});
+};
+
+export async function isNoteUpdatedEventFired(
+	host: Host,
+	user: { i: string },
+	noteId: string,
+	trigger: () => Promise<unknown>,
+	cond: (msg: Parameters<Misskey.StreamEvents['noteUpdated']>[0]) => boolean,
+): Promise<boolean> {
+	return new Promise<boolean>(async (resolve, reject) => {
+		// @ts-expect-error TODO: why?
+		const stream = new Misskey.Stream(`wss://${host}`, { token: user.i }, { WebSocket });
+		stream.send('s', { id: noteId });
+		stream.on('noteUpdated', msg => {
+			if (cond(msg)) {
+				stream.close();
+				clearTimeout(timer);
+				resolve(true);
+			}
+		});
+
+		let timer: NodeJS.Timeout | undefined;
+
+		await trigger().then(() => {
+			timer = setTimeout(() => {
+				stream.close();
+				resolve(false);
+			}, 500);
+		}).catch(err => {
+			stream.close();
+			clearTimeout(timer);
+			reject(err);
+		});
+	});
+};
+
+export async function assertNotificationReceived(
+	receiverHost: Host,
+	receiver: LoginUser,
+	trigger: () => Promise<unknown>,
+	cond: (notification: Misskey.entities.Notification) => boolean,
+	expect: boolean,
+) {
+	const streamingFired = await isFired(receiverHost, receiver, 'main', trigger, 'notification', cond);
+	strictEqual(streamingFired, expect);
+
+	const endpointFired = await receiver.client.request('i/notifications', {})
+		// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
+		.then(([notification]) => notification != null ? cond(notification) : false);
+	strictEqual(endpointFired, expect);
+}
diff --git a/packages/backend/test-federation/tsconfig.json b/packages/backend/test-federation/tsconfig.json
new file mode 100644
index 0000000000..3a1cb3b9f3
--- /dev/null
+++ b/packages/backend/test-federation/tsconfig.json
@@ -0,0 +1,114 @@
+{
+	"compilerOptions": {
+		/* Visit https://aka.ms/tsconfig to read more about this file */
+
+		/* Projects */
+		// "incremental": true,                              /* Save .tsbuildinfo files to allow for incremental compilation of projects. */
+		// "composite": true,                                /* Enable constraints that allow a TypeScript project to be used with project references. */
+		// "tsBuildInfoFile": "./.tsbuildinfo",              /* Specify the path to .tsbuildinfo incremental compilation file. */
+		// "disableSourceOfProjectReferenceRedirect": true,  /* Disable preferring source files instead of declaration files when referencing composite projects. */
+		// "disableSolutionSearching": true,                 /* Opt a project out of multi-project reference checking when editing. */
+		// "disableReferencedProjectLoad": true,             /* Reduce the number of projects loaded automatically by TypeScript. */
+
+		/* Language and Environment */
+		"target": "ESNext",                                  /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
+		// "lib": [],                                        /* Specify a set of bundled library declaration files that describe the target runtime environment. */
+		// "jsx": "preserve",                                /* Specify what JSX code is generated. */
+		// "experimentalDecorators": true,                   /* Enable experimental support for legacy experimental decorators. */
+		// "emitDecoratorMetadata": true,                    /* Emit design-type metadata for decorated declarations in source files. */
+		// "jsxFactory": "",                                 /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */
+		// "jsxFragmentFactory": "",                         /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
+		// "jsxImportSource": "",                            /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */
+		// "reactNamespace": "",                             /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */
+		// "noLib": true,                                    /* Disable including any library files, including the default lib.d.ts. */
+		// "useDefineForClassFields": true,                  /* Emit ECMAScript-standard-compliant class fields. */
+		// "moduleDetection": "auto",                        /* Control what method is used to detect module-format JS files. */
+
+		/* Modules */
+		"module": "NodeNext",                                /* Specify what module code is generated. */
+		// "rootDir": "./",                                  /* Specify the root folder within your source files. */
+		// "moduleResolution": "node10",                     /* Specify how TypeScript looks up a file from a given module specifier. */
+		// "baseUrl": "./",                                  /* Specify the base directory to resolve non-relative module names. */
+		// "paths": {},                                      /* Specify a set of entries that re-map imports to additional lookup locations. */
+		// "rootDirs": [],                                   /* Allow multiple folders to be treated as one when resolving modules. */
+		// "typeRoots": [],                                  /* Specify multiple folders that act like './node_modules/@types'. */
+		// "types": [],                                      /* Specify type package names to be included without being referenced in a source file. */
+		// "allowUmdGlobalAccess": true,                     /* Allow accessing UMD globals from modules. */
+		// "moduleSuffixes": [],                             /* List of file name suffixes to search when resolving a module. */
+		// "allowImportingTsExtensions": true,               /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */
+		// "resolvePackageJsonExports": true,                /* Use the package.json 'exports' field when resolving package imports. */
+		// "resolvePackageJsonImports": true,                /* Use the package.json 'imports' field when resolving imports. */
+		// "customConditions": [],                           /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */
+		// "noUncheckedSideEffectImports": true,             /* Check side effect imports. */
+		// "resolveJsonModule": true,                        /* Enable importing .json files. */
+		// "allowArbitraryExtensions": true,                 /* Enable importing files with any extension, provided a declaration file is present. */
+		// "noResolve": true,                                /* Disallow 'import's, 'require's or '<reference>'s from expanding the number of files TypeScript should add to a project. */
+
+		/* JavaScript Support */
+		// "allowJs": true,                                  /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */
+		// "checkJs": true,                                  /* Enable error reporting in type-checked JavaScript files. */
+		// "maxNodeModuleJsDepth": 1,                        /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */
+
+		/* Emit */
+		// "declaration": true,                              /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
+		// "declarationMap": true,                           /* Create sourcemaps for d.ts files. */
+		// "emitDeclarationOnly": true,                      /* Only output d.ts files and not JavaScript files. */
+		// "sourceMap": true,                                /* Create source map files for emitted JavaScript files. */
+		// "inlineSourceMap": true,                          /* Include sourcemap files inside the emitted JavaScript. */
+		// "noEmit": true,                                   /* Disable emitting files from a compilation. */
+		// "outFile": "./",                                  /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */
+		"outDir": "./built",                                 /* Specify an output folder for all emitted files. */
+		// "removeComments": true,                           /* Disable emitting comments. */
+		// "importHelpers": true,                            /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
+		// "downlevelIteration": true,                       /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
+		// "sourceRoot": "",                                 /* Specify the root path for debuggers to find the reference source code. */
+		// "mapRoot": "",                                    /* Specify the location where debugger should locate map files instead of generated locations. */
+		// "inlineSources": true,                            /* Include source code in the sourcemaps inside the emitted JavaScript. */
+		// "emitBOM": true,                                  /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
+		// "newLine": "crlf",                                /* Set the newline character for emitting files. */
+		// "stripInternal": true,                            /* Disable emitting declarations that have '@internal' in their JSDoc comments. */
+		// "noEmitHelpers": true,                            /* Disable generating custom helper functions like '__extends' in compiled output. */
+		// "noEmitOnError": true,                            /* Disable emitting files if any type checking errors are reported. */
+		// "preserveConstEnums": true,                       /* Disable erasing 'const enum' declarations in generated code. */
+		// "declarationDir": "./",                           /* Specify the output directory for generated declaration files. */
+
+		/* Interop Constraints */
+		// "isolatedModules": true,                          /* Ensure that each file can be safely transpiled without relying on other imports. */
+		// "verbatimModuleSyntax": true,                     /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */
+		// "isolatedDeclarations": true,                     /* Require sufficient annotation on exports so other tools can trivially generate declaration files. */
+		// "allowSyntheticDefaultImports": true,             /* Allow 'import x from y' when a module doesn't have a default export. */
+		"esModuleInterop": true,                             /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
+		// "preserveSymlinks": true,                         /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
+		"forceConsistentCasingInFileNames": true,            /* Ensure that casing is correct in imports. */
+
+		/* Type Checking */
+		"strict": true,                                      /* Enable all strict type-checking options. */
+		// "noImplicitAny": true,                            /* Enable error reporting for expressions and declarations with an implied 'any' type. */
+		// "strictNullChecks": true,                         /* When type checking, take into account 'null' and 'undefined'. */
+		// "strictFunctionTypes": true,                      /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
+		// "strictBindCallApply": true,                      /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */
+		// "strictPropertyInitialization": true,             /* Check for class properties that are declared but not set in the constructor. */
+		// "strictBuiltinIteratorReturn": true,              /* Built-in iterators are instantiated with a 'TReturn' type of 'undefined' instead of 'any'. */
+		// "noImplicitThis": true,                           /* Enable error reporting when 'this' is given the type 'any'. */
+		// "useUnknownInCatchVariables": true,               /* Default catch clause variables as 'unknown' instead of 'any'. */
+		// "alwaysStrict": true,                             /* Ensure 'use strict' is always emitted. */
+		// "noUnusedLocals": true,                           /* Enable error reporting when local variables aren't read. */
+		// "noUnusedParameters": true,                       /* Raise an error when a function parameter isn't read. */
+		// "exactOptionalPropertyTypes": true,               /* Interpret optional property types as written, rather than adding 'undefined'. */
+		// "noImplicitReturns": true,                        /* Enable error reporting for codepaths that do not explicitly return in a function. */
+		// "noFallthroughCasesInSwitch": true,               /* Enable error reporting for fallthrough cases in switch statements. */
+		// "noUncheckedIndexedAccess": true,                 /* Add 'undefined' to a type when accessed using an index. */
+		// "noImplicitOverride": true,                       /* Ensure overriding members in derived classes are marked with an override modifier. */
+		// "noPropertyAccessFromIndexSignature": true,       /* Enforces using indexed accessors for keys declared using an indexed type. */
+		// "allowUnusedLabels": true,                        /* Disable error reporting for unused labels. */
+		// "allowUnreachableCode": true,                     /* Disable error reporting for unreachable code. */
+
+		/* Completeness */
+		// "skipDefaultLibCheck": true,                      /* Skip type checking .d.ts files that are included with TypeScript. */
+		"skipLibCheck": true                                 /* Skip type checking all .d.ts files. */
+	},
+	"include": [
+		"daemon.ts",
+		"./test/**/*.ts"
+	]
+}
diff --git a/packages/shared/eslint.config.js b/packages/shared/eslint.config.js
index e9d27c4a72..0368d008c0 100644
--- a/packages/shared/eslint.config.js
+++ b/packages/shared/eslint.config.js
@@ -6,6 +6,7 @@ export default [
 	{
 		files: ['**/*.cjs'],
 		languageOptions: {
+			sourceType: 'commonjs',
 			parserOptions: {
 				sourceType: 'commonjs',
 			},
@@ -25,4 +26,10 @@ export default [
 			globals: globals.node,
 		},
 	},
+	{
+		files: ['**/*.js', '**/*.cjs'],
+		rules: {
+			'@typescript-eslint/no-var-requires': 'off',
+		},
+	},
 ];

From d2e8dc4fe3c6e90e68001ed1f092d4e3d2454283 Mon Sep 17 00:00:00 2001
From: "github-actions[bot]" <github-actions[bot]@users.noreply.github.com>
Date: Tue, 15 Oct 2024 04:53:43 +0000
Subject: [PATCH 121/121] Release: 2024.10.1

---
 package.json                     | 2 +-
 packages/misskey-js/package.json | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/package.json b/package.json
index 59b75fece4..8bf96d916d 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
 	"name": "misskey",
-	"version": "2024.10.1-beta.6",
+	"version": "2024.10.1",
 	"codename": "nasubi",
 	"repository": {
 		"type": "git",
diff --git a/packages/misskey-js/package.json b/packages/misskey-js/package.json
index 0f8433fbb1..a0a46a1162 100644
--- a/packages/misskey-js/package.json
+++ b/packages/misskey-js/package.json
@@ -1,7 +1,7 @@
 {
 	"type": "module",
 	"name": "misskey-js",
-	"version": "2024.10.1-beta.6",
+	"version": "2024.10.1",
 	"description": "Misskey SDK for JavaScript",
 	"license": "MIT",
 	"main": "./built/index.js",