From 1b064d7e30899827a59535ffa5fe6c90512a836d Mon Sep 17 00:00:00 2001
From: tamaina <tamaina@hotmail.co.jp>
Date: Sat, 9 Mar 2024 04:10:17 +0000
Subject: [PATCH 01/14] =?UTF-8?q?chore(backend):=20validateNote=E3=81=AE?=
 =?UTF-8?q?=E7=B5=90=E6=9E=9CError=E3=81=AF=E3=81=9D=E3=81=AE=E3=81=BE?=
 =?UTF-8?q?=E3=81=BEthrow=E3=81=99=E3=82=8B=20=E7=90=86=E7=94=B1=E3=81=8C?=
 =?UTF-8?q?=E3=82=8F=E3=81=8B=E3=82=89=E3=81=AA=E3=81=84=E3=81=9F=E3=82=81?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 packages/backend/src/core/activitypub/models/ApNoteService.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/packages/backend/src/core/activitypub/models/ApNoteService.ts b/packages/backend/src/core/activitypub/models/ApNoteService.ts
index b2fd435f93..4d64b08e15 100644
--- a/packages/backend/src/core/activitypub/models/ApNoteService.ts
+++ b/packages/backend/src/core/activitypub/models/ApNoteService.ts
@@ -129,7 +129,7 @@ export class ApNoteService {
 				value,
 				object,
 			});
-			throw new Error('invalid note');
+			throw err;
 		}
 
 		const note = object as IPost;

From db29680e749fae8ce93f82b527bf8e8597fa0526 Mon Sep 17 00:00:00 2001
From: syuilo <4439005+syuilo@users.noreply.github.com>
Date: Sat, 9 Mar 2024 15:31:21 +0900
Subject: [PATCH 02/14] chore(dev): remove deprecated vscode plugins

---
 .devcontainer/devcontainer.json | 1 -
 .vscode/extensions.json         | 1 -
 2 files changed, 2 deletions(-)

diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json
index e409adf644..f8d9905ecd 100644
--- a/.devcontainer/devcontainer.json
+++ b/.devcontainer/devcontainer.json
@@ -19,7 +19,6 @@
 				"editorconfig.editorconfig",
 				"dbaeumer.vscode-eslint",
 				"Vue.volar",
-				"Vue.vscode-typescript-vue-plugin",
 				"Orta.vscode-jest",
 				"dbaeumer.vscode-eslint",
 				"mrmlnc.vscode-json5"
diff --git a/.vscode/extensions.json b/.vscode/extensions.json
index baca8db246..d08109477c 100644
--- a/.vscode/extensions.json
+++ b/.vscode/extensions.json
@@ -3,7 +3,6 @@
 		"editorconfig.editorconfig",
 		"dbaeumer.vscode-eslint",
 		"Vue.volar",
-		"Vue.vscode-typescript-vue-plugin",
 		"Orta.vscode-jest",
 		"dbaeumer.vscode-eslint",
 		"mrmlnc.vscode-json5"

From dbc4fd3e93a7b73be9e0bc3e863cf1d553edd80c Mon Sep 17 00:00:00 2001
From: syuilo <4439005+syuilo@users.noreply.github.com>
Date: Sat, 9 Mar 2024 15:40:21 +0900
Subject: [PATCH 03/14] Update about-misskey.vue

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

diff --git a/packages/frontend/src/pages/about-misskey.vue b/packages/frontend/src/pages/about-misskey.vue
index 1a49dbf1d5..9aaa2d8fba 100644
--- a/packages/frontend/src/pages/about-misskey.vue
+++ b/packages/frontend/src/pages/about-misskey.vue
@@ -324,6 +324,7 @@ const patrons = [
 	'てば',
 	'たっくん',
 	'SHO SEKIGUCHI',
+	'塩キャベツ',
 ];
 
 const thereIsTreasure = ref($i && !claimedAchievements.includes('foundTreasure'));

From e4eaf1220e0d23b056f3203b3b24d8e3827134b9 Mon Sep 17 00:00:00 2001
From: FineArchs <133759614+FineArchs@users.noreply.github.com>
Date: Sat, 9 Mar 2024 17:55:41 +0900
Subject: [PATCH 04/14] Update example.yml (#13551)

---
 .config/example.yml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/.config/example.yml b/.config/example.yml
index 7fea929374..b0b7f14059 100644
--- a/.config/example.yml
+++ b/.config/example.yml
@@ -38,7 +38,7 @@
 # Option 3: If neither of the above applies to you.
 #           (In this case, the source code should be published
 #           on the Misskey interface.  IT IS NOT ENOUGH TO
-#           DISCLOSE THE SOURCE CODE WEHN A USER REQUESTS IT BY
+#           DISCLOSE THE SOURCE CODE WHEN A USER REQUESTS IT BY
 #           E-MAIL OR OTHER MEANS.  If you are not satisfied
 #           with this, it is recommended that you read the
 #           license again carefully.  Anyway, enabling this

From 6b676a928d3e167c98f2a1854a432adf5c125d65 Mon Sep 17 00:00:00 2001
From: yupix <yupi0982@outlook.jp>
Date: Sun, 10 Mar 2024 17:31:39 +0900
Subject: [PATCH 05/14] =?UTF-8?q?enhance(backend):=20antennas/update?=
 =?UTF-8?q?=E3=81=AE=E5=BF=85=E9=A0=88=E9=A0=85=E7=9B=AE=E3=82=92antennaId?=
 =?UTF-8?q?=E3=81=AE=E3=81=BF=E3=81=AB=20(#13542)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

* refactor: antennas/updateの必須項目を最小限に

* fix: userListIdがnullにできない
---
 CHANGELOG.md                                   |  2 +-
 .../server/api/endpoints/antennas/update.ts    | 12 +++++++-----
 packages/misskey-js/src/autogen/types.ts       | 18 +++++++++---------
 3 files changed, 17 insertions(+), 15 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index be621e1ebd..8c5f05dbe5 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -26,7 +26,7 @@
 - Fix: カスタム絵文字の画像読み込みに失敗した際はテキストではなくダミー画像を表示 #13487
 
 ### Server
--
+- Enhance: エンドポイント`antennas/update`の必須項目を`antennaId`のみに
 
 ## 2024.3.0
 
diff --git a/packages/backend/src/server/api/endpoints/antennas/update.ts b/packages/backend/src/server/api/endpoints/antennas/update.ts
index 459729f61f..76a34924a0 100644
--- a/packages/backend/src/server/api/endpoints/antennas/update.ts
+++ b/packages/backend/src/server/api/endpoints/antennas/update.ts
@@ -67,7 +67,7 @@ export const paramDef = {
 		withFile: { type: 'boolean' },
 		notify: { type: 'boolean' },
 	},
-	required: ['antennaId', 'name', 'src', 'keywords', 'excludeKeywords', 'users', 'caseSensitive', 'withReplies', 'withFile', 'notify'],
+	required: ['antennaId'],
 } as const;
 
 @Injectable()
@@ -83,8 +83,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 		private globalEventService: GlobalEventService,
 	) {
 		super(meta, paramDef, async (ps, me) => {
-			if (ps.keywords.flat().every(x => x === '') && ps.excludeKeywords.flat().every(x => x === '')) {
-				throw new Error('either keywords or excludeKeywords is required.');
+			if (ps.keywords && ps.excludeKeywords) {
+				if (ps.keywords.flat().every(x => x === '') && ps.excludeKeywords.flat().every(x => x === '')) {
+					throw new Error('either keywords or excludeKeywords is required.');
+				}
 			}
 			// Fetch the antenna
 			const antenna = await this.antennasRepository.findOneBy({
@@ -98,7 +100,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 
 			let userList;
 
-			if (ps.src === 'list' && ps.userListId) {
+			if ((ps.src === 'list' || antenna.src === 'list') && ps.userListId) {
 				userList = await this.userListsRepository.findOneBy({
 					id: ps.userListId,
 					userId: me.id,
@@ -112,7 +114,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 			await this.antennasRepository.update(antenna.id, {
 				name: ps.name,
 				src: ps.src,
-				userListId: userList ? userList.id : null,
+				userListId: ps.userListId !== undefined ? userList ? userList.id : null : undefined,
 				keywords: ps.keywords,
 				excludeKeywords: ps.excludeKeywords,
 				users: ps.users,
diff --git a/packages/misskey-js/src/autogen/types.ts b/packages/misskey-js/src/autogen/types.ts
index 41c3f50135..be3e86bd78 100644
--- a/packages/misskey-js/src/autogen/types.ts
+++ b/packages/misskey-js/src/autogen/types.ts
@@ -9925,19 +9925,19 @@ export type operations = {
         'application/json': {
           /** Format: misskey:id */
           antennaId: string;
-          name: string;
+          name?: string;
           /** @enum {string} */
-          src: 'home' | 'all' | 'users' | 'list' | 'users_blacklist';
+          src?: 'home' | 'all' | 'users' | 'list' | 'users_blacklist';
           /** Format: misskey:id */
           userListId?: string | null;
-          keywords: string[][];
-          excludeKeywords: string[][];
-          users: string[];
-          caseSensitive: boolean;
+          keywords?: string[][];
+          excludeKeywords?: string[][];
+          users?: string[];
+          caseSensitive?: boolean;
           localOnly?: boolean;
-          withReplies: boolean;
-          withFile: boolean;
-          notify: boolean;
+          withReplies?: boolean;
+          withFile?: boolean;
+          notify?: boolean;
         };
       };
     };

From e23e2f4ae9368754e4eadcd0afaa2bb1f984ba6a 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, 12 Mar 2024 12:09:26 +0900
Subject: [PATCH 06/14] Fix Changelog

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

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8c5f05dbe5..9c2a32aa56 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -12,7 +12,7 @@
 - Fix: 周年の実績が閏年を考慮しない問題を修正
 
 ### Server
--
+- Enhance: エンドポイント`antennas/update`の必須項目を`antennaId`のみに
 
 ## 2024.3.1
 
@@ -26,7 +26,7 @@
 - Fix: カスタム絵文字の画像読み込みに失敗した際はテキストではなくダミー画像を表示 #13487
 
 ### Server
-- Enhance: エンドポイント`antennas/update`の必須項目を`antennaId`のみに
+-
 
 ## 2024.3.0
 

From b280faa8e72c29036ef65af7fd8949538ab43dbd 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, 12 Mar 2024 13:48:14 +0900
Subject: [PATCH 07/14] =?UTF-8?q?enhance(frontend):=20=E5=90=84=E3=82=B5?=
 =?UTF-8?q?=E3=83=BC=E3=83=90=E3=83=BC=E3=81=AFMisskey=E3=82=92=E5=88=A9?=
 =?UTF-8?q?=E7=94=A8=E3=81=97=E3=81=9F=E3=82=B5=E3=83=BC=E3=83=93=E3=82=B9?=
 =?UTF-8?q?=E3=81=A7=E3=81=82=E3=82=8B=E3=81=93=E3=81=A8=E3=82=92=E5=BC=B7?=
 =?UTF-8?q?=E8=AA=BF=20(#13559)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

* ロゴ周りを追加

* 調整

---------

Co-authored-by: uboar <10250330+uboar@users.noreply.github.com>
---
 .../frontend/src/pages/welcome.entrance.a.vue | 27 ++++++++++++++-----
 1 file changed, 20 insertions(+), 7 deletions(-)

diff --git a/packages/frontend/src/pages/welcome.entrance.a.vue b/packages/frontend/src/pages/welcome.entrance.a.vue
index 89bb010dd6..6c05aad24f 100644
--- a/packages/frontend/src/pages/welcome.entrance.a.vue
+++ b/packages/frontend/src/pages/welcome.entrance.a.vue
@@ -9,7 +9,10 @@ SPDX-License-Identifier: AGPL-3.0-only
 	<XTimeline class="tl"/>
 	<div class="shape1"></div>
 	<div class="shape2"></div>
-	<img :src="misskeysvg" class="misskey"/>
+	<div class="logo-wrapper">
+		<div class="powered-by">Powered by</div>
+		<img :src="misskeysvg" class="misskey"/>
+	</div>
 	<div class="emojis">
 		<MkEmoji :normal="true" :noStyle="true" emoji="👍"/>
 		<MkEmoji :normal="true" :noStyle="true" emoji="❤"/>
@@ -113,14 +116,24 @@ misskeyApiGet('federation/instances', {
 		opacity: 0.5;
 	}
 
-	> .misskey {
+	> .logo-wrapper {
 		position: fixed;
-		top: 42px;
-		left: 42px;
-		width: 140px;
+		top: 36px;
+		left: 36px;
+		flex: auto;
+		color: #fff;
+		user-select: none;
+		pointer-events: none;
 
-		@media (max-width: 450px) {
-			width: 130px;
+		> .powered-by {
+			margin-bottom: 2px;
+		}
+
+		> .misskey {
+			width: 140px;
+			@media (max-width: 450px) {
+				width: 130px;
+			}
 		}
 	}
 

From 6d9c234cb6d3ddfaa3266255e7c305b329a556b6 Mon Sep 17 00:00:00 2001
From: anatawa12 <anatawa12@icloud.com>
Date: Tue, 12 Mar 2024 13:50:24 +0900
Subject: [PATCH 08/14] fix: URL preview popup for local URL appears in the
 upper left corner (#13555)

---
 CHANGELOG.md                                      | 1 +
 packages/frontend/src/components/MkLink.vue       | 4 ++--
 packages/frontend/src/components/global/MkA.vue   | 8 ++++++--
 packages/frontend/src/components/global/MkUrl.vue | 2 +-
 4 files changed, 10 insertions(+), 5 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9c2a32aa56..83d0a3f7d2 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,6 +10,7 @@
 - Enhance: リアクション受け入れが「いいねのみ」の場合はリアクション絵文字一覧を表示しないように
 - Fix: 一部のページ内リンクが正しく動作しない問題を修正
 - Fix: 周年の実績が閏年を考慮しない問題を修正
+- Fix: ローカルURLのプレビューポップアップが左上に表示される
 
 ### Server
 - Enhance: エンドポイント`antennas/update`の必須項目を`antennaId`のみに
diff --git a/packages/frontend/src/components/MkLink.vue b/packages/frontend/src/components/MkLink.vue
index a5abbeceac..3f7aba2fe4 100644
--- a/packages/frontend/src/components/MkLink.vue
+++ b/packages/frontend/src/components/MkLink.vue
@@ -29,13 +29,13 @@ const self = props.url.startsWith(local);
 const attr = self ? 'to' : 'href';
 const target = self ? null : '_blank';
 
-const el = ref<HTMLElement>();
+const el = ref<HTMLElement | { $el: HTMLElement }>();
 
 useTooltip(el, (showing) => {
 	os.popup(defineAsyncComponent(() => import('@/components/MkUrlPreviewPopup.vue')), {
 		showing,
 		url: props.url,
-		source: el.value,
+		source: el.value instanceof HTMLElement ? el.value : el.value?.$el,
 	}, {}, 'closed');
 });
 </script>
diff --git a/packages/frontend/src/components/global/MkA.vue b/packages/frontend/src/components/global/MkA.vue
index 61d7ac17d9..1ba7cb2022 100644
--- a/packages/frontend/src/components/global/MkA.vue
+++ b/packages/frontend/src/components/global/MkA.vue
@@ -4,13 +4,13 @@ SPDX-License-Identifier: AGPL-3.0-only
 -->
 
 <template>
-<a :href="to" :class="active ? activeClass : null" @click.prevent="nav" @contextmenu.prevent.stop="onContextmenu">
+<a ref="el" :href="to" :class="active ? activeClass : null" @click.prevent="nav" @contextmenu.prevent.stop="onContextmenu">
 	<slot></slot>
 </a>
 </template>
 
 <script lang="ts" setup>
-import { computed } from 'vue';
+import { computed, shallowRef } from 'vue';
 import * as os from '@/os.js';
 import copyToClipboard from '@/scripts/copy-to-clipboard.js';
 import { url } from '@/config.js';
@@ -26,6 +26,10 @@ const props = withDefaults(defineProps<{
 	behavior: null,
 });
 
+const el = shallowRef<HTMLElement>();
+
+defineExpose({ $el: el });
+
 const router = useRouter();
 
 const active = computed(() => {
diff --git a/packages/frontend/src/components/global/MkUrl.vue b/packages/frontend/src/components/global/MkUrl.vue
index 0c3eee63ff..8d29a4da8c 100644
--- a/packages/frontend/src/components/global/MkUrl.vue
+++ b/packages/frontend/src/components/global/MkUrl.vue
@@ -49,7 +49,7 @@ if (props.showUrlPreview) {
 		os.popup(defineAsyncComponent(() => import('@/components/MkUrlPreviewPopup.vue')), {
 			showing,
 			url: props.url,
-			source: el.value,
+			source: el.value instanceof HTMLElement ? el.value : el.value?.$el,
 		}, {}, 'closed');
 	});
 }

From 5c1d86b796d6ab878bc4f9bd2faf4207998e71cf 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: Tue, 12 Mar 2024 14:31:34 +0900
Subject: [PATCH 09/14] =?UTF-8?q?refactor(backend):=20UserEntityService.pa?=
 =?UTF-8?q?ckMany()=E3=81=AE=E9=AB=98=E9=80=9F=E5=8C=96=20(#13550)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

* refactor(backend): UserEntityService.packMany()の高速化

* 修正
---
 .../src/core/entities/UserEntityService.ts    | 229 +++++++-
 .../server/api/endpoints/users/relation.ts    |   8 +-
 .../test/unit/entities/UserEntityService.ts   | 528 ++++++++++++++++++
 3 files changed, 729 insertions(+), 36 deletions(-)
 create mode 100644 packages/backend/test/unit/entities/UserEntityService.ts

diff --git a/packages/backend/src/core/entities/UserEntityService.ts b/packages/backend/src/core/entities/UserEntityService.ts
index 14761357a5..df2b27d709 100644
--- a/packages/backend/src/core/entities/UserEntityService.ts
+++ b/packages/backend/src/core/entities/UserEntityService.ts
@@ -7,6 +7,7 @@ import { Inject, Injectable } from '@nestjs/common';
 import * as Redis from 'ioredis';
 import _Ajv from 'ajv';
 import { ModuleRef } from '@nestjs/core';
+import { In } from 'typeorm';
 import { DI } from '@/di-symbols.js';
 import type { Config } from '@/config.js';
 import type { Packed } from '@/misc/json-schema.js';
@@ -14,9 +15,30 @@ import type { Promiseable } from '@/misc/prelude/await-all.js';
 import { awaitAll } from '@/misc/prelude/await-all.js';
 import { USER_ACTIVE_THRESHOLD, USER_ONLINE_THRESHOLD } from '@/const.js';
 import type { MiLocalUser, MiPartialLocalUser, MiPartialRemoteUser, MiRemoteUser, MiUser } from '@/models/User.js';
-import { birthdaySchema, descriptionSchema, localUsernameSchema, locationSchema, nameSchema, passwordSchema } from '@/models/User.js';
-import { MiNotification } from '@/models/Notification.js';
-import type { UsersRepository, UserSecurityKeysRepository, FollowingsRepository, FollowRequestsRepository, BlockingsRepository, MutingsRepository, DriveFilesRepository, NoteUnreadsRepository, UserNotePiningsRepository, UserProfilesRepository, AnnouncementReadsRepository, AnnouncementsRepository, MiUserProfile, RenoteMutingsRepository, UserMemoRepository } from '@/models/_.js';
+import {
+	birthdaySchema,
+	descriptionSchema,
+	localUsernameSchema,
+	locationSchema,
+	nameSchema,
+	passwordSchema,
+} from '@/models/User.js';
+import type {
+	BlockingsRepository,
+	FollowingsRepository,
+	FollowRequestsRepository,
+	MiFollowing,
+	MiUserNotePining,
+	MiUserProfile,
+	MutingsRepository,
+	NoteUnreadsRepository,
+	RenoteMutingsRepository,
+	UserMemoRepository,
+	UserNotePiningsRepository,
+	UserProfilesRepository,
+	UserSecurityKeysRepository,
+	UsersRepository,
+} from '@/models/_.js';
 import { bindThis } from '@/decorators.js';
 import { RoleService } from '@/core/RoleService.js';
 import { ApPersonService } from '@/core/activitypub/models/ApPersonService.js';
@@ -46,11 +68,23 @@ function isRemoteUser(user: MiUser | { host: MiUser['host'] }): boolean {
 	return !isLocalUser(user);
 }
 
+export type UserRelation = {
+	id: MiUser['id']
+	following: MiFollowing | null,
+	isFollowing: boolean
+	isFollowed: boolean
+	hasPendingFollowRequestFromYou: boolean
+	hasPendingFollowRequestToYou: boolean
+	isBlocking: boolean
+	isBlocked: boolean
+	isMuted: boolean
+	isRenoteMuted: boolean
+}
+
 @Injectable()
 export class UserEntityService implements OnModuleInit {
 	private apPersonService: ApPersonService;
 	private noteEntityService: NoteEntityService;
-	private driveFileEntityService: DriveFileEntityService;
 	private pageEntityService: PageEntityService;
 	private customEmojiService: CustomEmojiService;
 	private announcementService: AnnouncementService;
@@ -89,9 +123,6 @@ export class UserEntityService implements OnModuleInit {
 		@Inject(DI.renoteMutingsRepository)
 		private renoteMutingsRepository: RenoteMutingsRepository,
 
-		@Inject(DI.driveFilesRepository)
-		private driveFilesRepository: DriveFilesRepository,
-
 		@Inject(DI.noteUnreadsRepository)
 		private noteUnreadsRepository: NoteUnreadsRepository,
 
@@ -101,12 +132,6 @@ export class UserEntityService implements OnModuleInit {
 		@Inject(DI.userProfilesRepository)
 		private userProfilesRepository: UserProfilesRepository,
 
-		@Inject(DI.announcementReadsRepository)
-		private announcementReadsRepository: AnnouncementReadsRepository,
-
-		@Inject(DI.announcementsRepository)
-		private announcementsRepository: AnnouncementsRepository,
-
 		@Inject(DI.userMemosRepository)
 		private userMemosRepository: UserMemoRepository,
 	) {
@@ -115,7 +140,6 @@ export class UserEntityService implements OnModuleInit {
 	onModuleInit() {
 		this.apPersonService = this.moduleRef.get('ApPersonService');
 		this.noteEntityService = this.moduleRef.get('NoteEntityService');
-		this.driveFileEntityService = this.moduleRef.get('DriveFileEntityService');
 		this.pageEntityService = this.moduleRef.get('PageEntityService');
 		this.customEmojiService = this.moduleRef.get('CustomEmojiService');
 		this.announcementService = this.moduleRef.get('AnnouncementService');
@@ -138,7 +162,7 @@ export class UserEntityService implements OnModuleInit {
 	public isRemoteUser = isRemoteUser;
 
 	@bindThis
-	public async getRelation(me: MiUser['id'], target: MiUser['id']) {
+	public async getRelation(me: MiUser['id'], target: MiUser['id']): Promise<UserRelation> {
 		const [
 			following,
 			isFollowed,
@@ -211,6 +235,59 @@ export class UserEntityService implements OnModuleInit {
 		};
 	}
 
+	@bindThis
+	public async getRelations(me: MiUser['id'], targets: MiUser['id'][]): Promise<Map<MiUser['id'], UserRelation>> {
+		const [
+			followers,
+			followees,
+			followersRequests,
+			followeesRequests,
+			blockers,
+			blockees,
+			muters,
+			renoteMuters,
+		] = await Promise.all([
+			this.followingsRepository.findBy({ followerId: me })
+				.then(f => new Map(f.map(it => [it.followeeId, it]))),
+			this.followingsRepository.findBy({ followeeId: me })
+				.then(it => it.map(it => it.followerId)),
+			this.followRequestsRepository.findBy({ followerId: me })
+				.then(it => it.map(it => it.followeeId)),
+			this.followRequestsRepository.findBy({ followeeId: me })
+				.then(it => it.map(it => it.followerId)),
+			this.blockingsRepository.findBy({ blockerId: me })
+				.then(it => it.map(it => it.blockeeId)),
+			this.blockingsRepository.findBy({ blockeeId: me })
+				.then(it => it.map(it => it.blockerId)),
+			this.mutingsRepository.findBy({ muterId: me })
+				.then(it => it.map(it => it.muteeId)),
+			this.renoteMutingsRepository.findBy({ muterId: me })
+				.then(it => it.map(it => it.muteeId)),
+		]);
+
+		return new Map(
+			targets.map(target => {
+				const following = followers.get(target) ?? null;
+
+				return [
+					target,
+					{
+						id: target,
+						following: following,
+						isFollowing: following != null,
+						isFollowed: followees.includes(target),
+						hasPendingFollowRequestFromYou: followersRequests.includes(target),
+						hasPendingFollowRequestToYou: followeesRequests.includes(target),
+						isBlocking: blockers.includes(target),
+						isBlocked: blockees.includes(target),
+						isMuted: muters.includes(target),
+						isRenoteMuted: renoteMuters.includes(target),
+					},
+				];
+			}),
+		);
+	}
+
 	@bindThis
 	public async getHasUnreadAntenna(userId: MiUser['id']): Promise<boolean> {
 		/*
@@ -303,6 +380,9 @@ export class UserEntityService implements OnModuleInit {
 			schema?: S,
 			includeSecrets?: boolean,
 			userProfile?: MiUserProfile,
+			userRelations?: Map<MiUser['id'], UserRelation>,
+			userMemos?: Map<MiUser['id'], string | null>,
+			pinNotes?: Map<MiUser['id'], MiUserNotePining[]>,
 		},
 	): Promise<Packed<S>> {
 		const opts = Object.assign({
@@ -317,13 +397,41 @@ export class UserEntityService implements OnModuleInit {
 		const isMe = meId === user.id;
 		const iAmModerator = me ? await this.roleService.isModerator(me as MiUser) : false;
 
-		const relation = meId && !isMe && isDetailed ? await this.getRelation(meId, user.id) : null;
-		const pins = isDetailed ? await this.userNotePiningsRepository.createQueryBuilder('pin')
-			.where('pin.userId = :userId', { userId: user.id })
-			.innerJoinAndSelect('pin.note', 'note')
-			.orderBy('pin.id', 'DESC')
-			.getMany() : [];
-		const profile = isDetailed ? (opts.userProfile ?? await this.userProfilesRepository.findOneByOrFail({ userId: user.id })) : null;
+		const profile = isDetailed
+			? (opts.userProfile ?? await this.userProfilesRepository.findOneByOrFail({ userId: user.id }))
+			: null;
+
+		let relation: UserRelation | null = null;
+		if (meId && !isMe && isDetailed) {
+			if (opts.userRelations) {
+				relation = opts.userRelations.get(user.id) ?? null;
+			} else {
+				relation = await this.getRelation(meId, user.id);
+			}
+		}
+
+		let memo: string | null = null;
+		if (isDetailed && meId) {
+			if (opts.userMemos) {
+				memo = opts.userMemos.get(user.id) ?? null;
+			} else {
+				memo = await this.userMemosRepository.findOneBy({ userId: meId, targetUserId: user.id })
+					.then(row => row?.memo ?? null);
+			}
+		}
+
+		let pins: MiUserNotePining[] = [];
+		if (isDetailed) {
+			if (opts.pinNotes) {
+				pins = opts.pinNotes.get(user.id) ?? [];
+			} else {
+				pins = await this.userNotePiningsRepository.createQueryBuilder('pin')
+					.where('pin.userId = :userId', { userId: user.id })
+					.innerJoinAndSelect('pin.note', 'note')
+					.orderBy('pin.id', 'DESC')
+					.getMany();
+			}
+		}
 
 		const followingCount = profile == null ? null :
 			(profile.followingVisibility === 'public') || isMe ? user.followingCount :
@@ -416,9 +524,7 @@ export class UserEntityService implements OnModuleInit {
 				twoFactorEnabled: profile!.twoFactorEnabled,
 				usePasswordLessLogin: profile!.usePasswordLessLogin,
 				securityKeys: profile!.twoFactorEnabled
-					? this.userSecurityKeysRepository.countBy({
-						userId: user.id,
-					}).then(result => result >= 1)
+					? 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,
@@ -430,10 +536,7 @@ export class UserEntityService implements OnModuleInit {
 					isAdministrator: role.isAdministrator,
 					displayOrder: role.displayOrder,
 				}))),
-				memo: meId == null ? null : await this.userMemosRepository.findOneBy({
-					userId: meId,
-					targetUserId: user.id,
-				}).then(row => row?.memo ?? null),
+				memo: memo,
 				moderationNote: iAmModerator ? (profile!.moderationNote ?? '') : undefined,
 			} : {}),
 
@@ -514,7 +617,7 @@ export class UserEntityService implements OnModuleInit {
 		return await awaitAll(packed);
 	}
 
-	public packMany<S extends 'MeDetailed' | 'UserDetailedNotMe' | 'UserDetailed' | 'UserLite' = 'UserLite'>(
+	public async packMany<S extends 'MeDetailed' | 'UserDetailedNotMe' | 'UserDetailed' | 'UserLite' = 'UserLite'>(
 		users: (MiUser['id'] | MiUser)[],
 		me?: { id: MiUser['id'] } | null | undefined,
 		options?: {
@@ -522,6 +625,70 @@ export class UserEntityService implements OnModuleInit {
 			includeSecrets?: boolean,
 		},
 	): Promise<Packed<S>[]> {
-		return Promise.all(users.map(u => this.pack(u, me, options)));
+		// -- IDのみの要素を補完して完全なエンティティ一覧を作る
+
+		const _users = users.filter((user): user is MiUser => typeof user !== 'string');
+		if (_users.length !== users.length) {
+			_users.push(
+				...await this.usersRepository.findBy({
+					id: In(users.filter((user): user is string => typeof user === 'string')),
+				}),
+			);
+		}
+		const _userIds = _users.map(u => u.id);
+
+		// -- 特に前提条件のない値群を取得
+
+		const profilesMap = await this.userProfilesRepository.findBy({ userId: In(_userIds) })
+			.then(profiles => new Map(profiles.map(p => [p.userId, p])));
+
+		// -- 実行者の有無や指定スキーマの種別によって要否が異なる値群を取得
+
+		let userRelations: Map<MiUser['id'], UserRelation> = new Map();
+		let userMemos: Map<MiUser['id'], string | null> = new Map();
+		let pinNotes: Map<MiUser['id'], MiUserNotePining[]> = new Map();
+
+		if (options?.schema !== 'UserLite') {
+			const meId = me ? me.id : null;
+			if (meId) {
+				userMemos = await this.userMemosRepository.findBy({ userId: meId })
+					.then(memos => new Map(memos.map(memo => [memo.targetUserId, memo.memo])));
+
+				if (_userIds.length > 0) {
+					userRelations = await this.getRelations(meId, _userIds);
+					pinNotes = await this.userNotePiningsRepository.createQueryBuilder('pin')
+						.where('pin.userId IN (:...userIds)', { userIds: _userIds })
+						.innerJoinAndSelect('pin.note', 'note')
+						.getMany()
+						.then(pinsNotes => {
+							const map = new Map<MiUser['id'], MiUserNotePining[]>();
+							for (const note of pinsNotes) {
+								const notes = map.get(note.userId) ?? [];
+								notes.push(note);
+								map.set(note.userId, notes);
+							}
+							for (const [, notes] of map.entries()) {
+								// pack側ではDESCで取得しているので、それに合わせて降順に並び替えておく
+								notes.sort((a, b) => b.id.localeCompare(a.id));
+							}
+							return map;
+						});
+				}
+			}
+		}
+
+		return Promise.all(
+			_users.map(u => this.pack(
+				u,
+				me,
+				{
+					...options,
+					userProfile: profilesMap.get(u.id),
+					userRelations: userRelations,
+					userMemos: userMemos,
+					pinNotes: pinNotes,
+				},
+			)),
+		);
 	}
 }
diff --git a/packages/backend/src/server/api/endpoints/users/relation.ts b/packages/backend/src/server/api/endpoints/users/relation.ts
index 6a5b2262fa..1d75437b81 100644
--- a/packages/backend/src/server/api/endpoints/users/relation.ts
+++ b/packages/backend/src/server/api/endpoints/users/relation.ts
@@ -132,11 +132,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 		private userEntityService: UserEntityService,
 	) {
 		super(meta, paramDef, async (ps, me) => {
-			const ids = Array.isArray(ps.userId) ? ps.userId : [ps.userId];
-
-			const relations = await Promise.all(ids.map(id => this.userEntityService.getRelation(me.id, id)));
-
-			return Array.isArray(ps.userId) ? relations : relations[0];
+			return Array.isArray(ps.userId)
+				? await this.userEntityService.getRelations(me.id, ps.userId).then(it => [...it.values()])
+				: await this.userEntityService.getRelation(me.id, ps.userId).then(it => [it]);
 		});
 	}
 }
diff --git a/packages/backend/test/unit/entities/UserEntityService.ts b/packages/backend/test/unit/entities/UserEntityService.ts
new file mode 100644
index 0000000000..ee16d421c4
--- /dev/null
+++ b/packages/backend/test/unit/entities/UserEntityService.ts
@@ -0,0 +1,528 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Test, TestingModule } from '@nestjs/testing';
+import { UserEntityService } from '@/core/entities/UserEntityService.js';
+import { GlobalModule } from '@/GlobalModule.js';
+import { CoreModule } from '@/core/CoreModule.js';
+import type { MiUser } from '@/models/User.js';
+import { secureRndstr } from '@/misc/secure-rndstr.js';
+import { genAidx } from '@/misc/id/aidx.js';
+import {
+	BlockingsRepository,
+	FollowingsRepository, FollowRequestsRepository,
+	MiUserProfile, MutingsRepository, RenoteMutingsRepository,
+	UserMemoRepository,
+	UserProfilesRepository,
+	UsersRepository,
+} from '@/models/_.js';
+import { DI } from '@/di-symbols.js';
+import { AvatarDecorationService } from '@/core/AvatarDecorationService.js';
+import { ApPersonService } from '@/core/activitypub/models/ApPersonService.js';
+import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
+import { PageEntityService } from '@/core/entities/PageEntityService.js';
+import { CustomEmojiService } from '@/core/CustomEmojiService.js';
+import { AnnouncementService } from '@/core/AnnouncementService.js';
+import { RoleService } from '@/core/RoleService.js';
+import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
+import { IdService } from '@/core/IdService.js';
+import { UtilityService } from '@/core/UtilityService.js';
+import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js';
+import { ModerationLogService } from '@/core/ModerationLogService.js';
+import { GlobalEventService } from '@/core/GlobalEventService.js';
+import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js';
+import { MetaService } from '@/core/MetaService.js';
+import { FetchInstanceMetadataService } from '@/core/FetchInstanceMetadataService.js';
+import { CacheService } from '@/core/CacheService.js';
+import { ApResolverService } from '@/core/activitypub/ApResolverService.js';
+import { ApNoteService } from '@/core/activitypub/models/ApNoteService.js';
+import { ApImageService } from '@/core/activitypub/models/ApImageService.js';
+import { ApMfmService } from '@/core/activitypub/ApMfmService.js';
+import { MfmService } from '@/core/MfmService.js';
+import { HashtagService } from '@/core/HashtagService.js';
+import UsersChart from '@/core/chart/charts/users.js';
+import { ChartLoggerService } from '@/core/chart/ChartLoggerService.js';
+import InstanceChart from '@/core/chart/charts/instance.js';
+import { ApLoggerService } from '@/core/activitypub/ApLoggerService.js';
+import { AccountMoveService } from '@/core/AccountMoveService.js';
+import { ReactionService } from '@/core/ReactionService.js';
+import { NotificationService } from '@/core/NotificationService.js';
+
+process.env.NODE_ENV = 'test';
+
+describe('UserEntityService', () => {
+	describe('pack/packMany', () => {
+		let app: TestingModule;
+		let service: UserEntityService;
+		let usersRepository: UsersRepository;
+		let userProfileRepository: UserProfilesRepository;
+		let userMemosRepository: UserMemoRepository;
+		let followingRepository: FollowingsRepository;
+		let followingRequestRepository: FollowRequestsRepository;
+		let blockingRepository: BlockingsRepository;
+		let mutingRepository: MutingsRepository;
+		let renoteMutingsRepository: RenoteMutingsRepository;
+
+		async function createUser(userData: Partial<MiUser> = {}, profileData: Partial<MiUserProfile> = {}) {
+			const un = secureRndstr(16);
+			const user = await usersRepository
+				.insert({
+					...userData,
+					id: genAidx(Date.now()),
+					username: un,
+					usernameLower: un,
+				})
+				.then(x => usersRepository.findOneByOrFail(x.identifiers[0]));
+
+			await userProfileRepository.insert({
+				...profileData,
+				userId: user.id,
+			});
+
+			return user;
+		}
+
+		async function memo(writer: MiUser, target: MiUser, memo: string) {
+			await userMemosRepository.insert({
+				id: genAidx(Date.now()),
+				userId: writer.id,
+				targetUserId: target.id,
+				memo,
+			});
+		}
+
+		async function follow(follower: MiUser, followee: MiUser) {
+			await followingRepository.insert({
+				id: genAidx(Date.now()),
+				followerId: follower.id,
+				followeeId: followee.id,
+			});
+		}
+
+		async function requestFollow(requester: MiUser, requestee: MiUser) {
+			await followingRequestRepository.insert({
+				id: genAidx(Date.now()),
+				followerId: requester.id,
+				followeeId: requestee.id,
+			});
+		}
+
+		async function block(blocker: MiUser, blockee: MiUser) {
+			await blockingRepository.insert({
+				id: genAidx(Date.now()),
+				blockerId: blocker.id,
+				blockeeId: blockee.id,
+			});
+		}
+
+		async function mute(mutant: MiUser, mutee: MiUser) {
+			await mutingRepository.insert({
+				id: genAidx(Date.now()),
+				muterId: mutant.id,
+				muteeId: mutee.id,
+			});
+		}
+
+		async function muteRenote(mutant: MiUser, mutee: MiUser) {
+			await renoteMutingsRepository.insert({
+				id: genAidx(Date.now()),
+				muterId: mutant.id,
+				muteeId: mutee.id,
+			});
+		}
+
+		function randomIntRange(weight = 10) {
+			return [...Array(Math.floor(Math.random() * weight))].map((it, idx) => idx);
+		}
+
+		beforeAll(async () => {
+			const services = [
+				UserEntityService,
+				ApPersonService,
+				NoteEntityService,
+				PageEntityService,
+				CustomEmojiService,
+				AnnouncementService,
+				RoleService,
+				FederatedInstanceService,
+				IdService,
+				AvatarDecorationService,
+				UtilityService,
+				EmojiEntityService,
+				ModerationLogService,
+				GlobalEventService,
+				DriveFileEntityService,
+				MetaService,
+				FetchInstanceMetadataService,
+				CacheService,
+				ApResolverService,
+				ApNoteService,
+				ApImageService,
+				ApMfmService,
+				MfmService,
+				HashtagService,
+				UsersChart,
+				ChartLoggerService,
+				InstanceChart,
+				ApLoggerService,
+				AccountMoveService,
+				ReactionService,
+				NotificationService,
+			];
+
+			app = await Test.createTestingModule({
+				imports: [GlobalModule, CoreModule],
+				providers: [
+					...services,
+					...services.map(x => ({ provide: x.name, useExisting: x })),
+				],
+			}).compile();
+			await app.init();
+			app.enableShutdownHooks();
+
+			service = app.get<UserEntityService>(UserEntityService);
+			usersRepository = app.get<UsersRepository>(DI.usersRepository);
+			userProfileRepository = app.get<UserProfilesRepository>(DI.userProfilesRepository);
+			userMemosRepository = app.get<UserMemoRepository>(DI.userMemosRepository);
+			followingRepository = app.get<FollowingsRepository>(DI.followingsRepository);
+			followingRequestRepository = app.get<FollowRequestsRepository>(DI.followRequestsRepository);
+			blockingRepository = app.get<BlockingsRepository>(DI.blockingsRepository);
+			mutingRepository = app.get<MutingsRepository>(DI.mutingsRepository);
+			renoteMutingsRepository = app.get<RenoteMutingsRepository>(DI.renoteMutingsRepository);
+		});
+
+		afterAll(async () => {
+			await app.close();
+		});
+
+		test('UserLite', async() => {
+			const me = await createUser();
+			const who = await createUser();
+
+			await memo(me, who, 'memo');
+
+			const actual = await service.pack(who, me, { schema: 'UserLite' }) as any;
+			// no detail
+			expect(actual.memo).toBeUndefined();
+			// no detail and me
+			expect(actual.birthday).toBeUndefined();
+			// no detail and me
+			expect(actual.achievements).toBeUndefined();
+		});
+
+		test('UserDetailedNotMe', async() => {
+			const me = await createUser();
+			const who = await createUser({}, { birthday: '2000-01-01' });
+
+			await memo(me, who, 'memo');
+
+			const actual = await service.pack(who, me, { schema: 'UserDetailedNotMe' }) as any;
+			// is detail
+			expect(actual.memo).toBe('memo');
+			// is detail
+			expect(actual.birthday).toBe('2000-01-01');
+			// no detail and me
+			expect(actual.achievements).toBeUndefined();
+		});
+
+		test('MeDetailed', async() => {
+			const achievements = [{ name: 'achievement', unlockedAt: new Date().getTime() }];
+			const me = await createUser({}, {
+				birthday: '2000-01-01',
+				achievements: achievements,
+			});
+			await memo(me, me, 'memo');
+
+			const actual = await service.pack(me, me, { schema: 'MeDetailed' }) as any;
+			// is detail
+			expect(actual.memo).toBe('memo');
+			// is detail
+			expect(actual.birthday).toBe('2000-01-01');
+			// is detail and me
+			expect(actual.achievements).toEqual(achievements);
+		});
+
+		describe('packManyによるpreloadがある時、preloadが無い時とpackの結果が同じになるか見たい', () => {
+			test('no-preload', async() => {
+				const me = await createUser();
+				// meがフォローしてる人たち
+				const followeeMe = await Promise.all(randomIntRange().map(() => createUser()));
+				for (const who of followeeMe) {
+					await follow(me, who);
+					const actual = await service.pack(who, me, { schema: 'UserDetailed' }) as any;
+					expect(actual.isFollowing).toBe(true);
+					expect(actual.isFollowed).toBe(false);
+					expect(actual.hasPendingFollowRequestFromYou).toBe(false);
+					expect(actual.hasPendingFollowRequestToYou).toBe(false);
+					expect(actual.isBlocking).toBe(false);
+					expect(actual.isBlocked).toBe(false);
+					expect(actual.isMuted).toBe(false);
+					expect(actual.isRenoteMuted).toBe(false);
+				}
+
+				// meをフォローしてる人たち
+				const followerMe = await Promise.all(randomIntRange().map(() => createUser()));
+				for (const who of followerMe) {
+					await follow(who, me);
+					const actual = await service.pack(who, me, { schema: 'UserDetailed' }) as any;
+					expect(actual.isFollowing).toBe(false);
+					expect(actual.isFollowed).toBe(true);
+					expect(actual.hasPendingFollowRequestFromYou).toBe(false);
+					expect(actual.hasPendingFollowRequestToYou).toBe(false);
+					expect(actual.isBlocking).toBe(false);
+					expect(actual.isBlocked).toBe(false);
+					expect(actual.isMuted).toBe(false);
+					expect(actual.isRenoteMuted).toBe(false);
+				}
+
+				// meがフォローリクエストを送った人たち
+				const requestsFromYou = await Promise.all(randomIntRange().map(() => createUser()));
+				for (const who of requestsFromYou) {
+					await requestFollow(me, who);
+					const actual = await service.pack(who, me, { schema: 'UserDetailed' }) as any;
+					expect(actual.isFollowing).toBe(false);
+					expect(actual.isFollowed).toBe(false);
+					expect(actual.hasPendingFollowRequestFromYou).toBe(true);
+					expect(actual.hasPendingFollowRequestToYou).toBe(false);
+					expect(actual.isBlocking).toBe(false);
+					expect(actual.isBlocked).toBe(false);
+					expect(actual.isMuted).toBe(false);
+					expect(actual.isRenoteMuted).toBe(false);
+				}
+
+				// meにフォローリクエストを送った人たち
+				const requestsToYou = await Promise.all(randomIntRange().map(() => createUser()));
+				for (const who of requestsToYou) {
+					await requestFollow(who, me);
+					const actual = await service.pack(who, me, { schema: 'UserDetailed' }) as any;
+					expect(actual.isFollowing).toBe(false);
+					expect(actual.isFollowed).toBe(false);
+					expect(actual.hasPendingFollowRequestFromYou).toBe(false);
+					expect(actual.hasPendingFollowRequestToYou).toBe(true);
+					expect(actual.isBlocking).toBe(false);
+					expect(actual.isBlocked).toBe(false);
+					expect(actual.isMuted).toBe(false);
+					expect(actual.isRenoteMuted).toBe(false);
+				}
+
+				// meがブロックしてる人たち
+				const blockingYou = await Promise.all(randomIntRange().map(() => createUser()));
+				for (const who of blockingYou) {
+					await block(me, who);
+					const actual = await service.pack(who, me, { schema: 'UserDetailed' }) as any;
+					expect(actual.isFollowing).toBe(false);
+					expect(actual.isFollowed).toBe(false);
+					expect(actual.hasPendingFollowRequestFromYou).toBe(false);
+					expect(actual.hasPendingFollowRequestToYou).toBe(false);
+					expect(actual.isBlocking).toBe(true);
+					expect(actual.isBlocked).toBe(false);
+					expect(actual.isMuted).toBe(false);
+					expect(actual.isRenoteMuted).toBe(false);
+				}
+
+				// meをブロックしてる人たち
+				const blockingMe = await Promise.all(randomIntRange().map(() => createUser()));
+				for (const who of blockingMe) {
+					await block(who, me);
+					const actual = await service.pack(who, me, { schema: 'UserDetailed' }) as any;
+					expect(actual.isFollowing).toBe(false);
+					expect(actual.isFollowed).toBe(false);
+					expect(actual.hasPendingFollowRequestFromYou).toBe(false);
+					expect(actual.hasPendingFollowRequestToYou).toBe(false);
+					expect(actual.isBlocking).toBe(false);
+					expect(actual.isBlocked).toBe(true);
+					expect(actual.isMuted).toBe(false);
+					expect(actual.isRenoteMuted).toBe(false);
+				}
+
+				// meがミュートしてる人たち
+				const muters = await Promise.all(randomIntRange().map(() => createUser()));
+				for (const who of muters) {
+					await mute(me, who);
+					const actual = await service.pack(who, me, { schema: 'UserDetailed' }) as any;
+					expect(actual.isFollowing).toBe(false);
+					expect(actual.isFollowed).toBe(false);
+					expect(actual.hasPendingFollowRequestFromYou).toBe(false);
+					expect(actual.hasPendingFollowRequestToYou).toBe(false);
+					expect(actual.isBlocking).toBe(false);
+					expect(actual.isBlocked).toBe(false);
+					expect(actual.isMuted).toBe(true);
+					expect(actual.isRenoteMuted).toBe(false);
+				}
+
+				// meがリノートミュートしてる人たち
+				const renoteMuters = await Promise.all(randomIntRange().map(() => createUser()));
+				for (const who of renoteMuters) {
+					await muteRenote(me, who);
+					const actual = await service.pack(who, me, { schema: 'UserDetailed' }) as any;
+					expect(actual.isFollowing).toBe(false);
+					expect(actual.isFollowed).toBe(false);
+					expect(actual.hasPendingFollowRequestFromYou).toBe(false);
+					expect(actual.hasPendingFollowRequestToYou).toBe(false);
+					expect(actual.isBlocking).toBe(false);
+					expect(actual.isBlocked).toBe(false);
+					expect(actual.isMuted).toBe(false);
+					expect(actual.isRenoteMuted).toBe(true);
+				}
+			});
+
+			test('preload', async() => {
+				const me = await createUser();
+
+				{
+					// meがフォローしてる人たち
+					const followeeMe = await Promise.all(randomIntRange().map(() => createUser()));
+					for (const who of followeeMe) {
+						await follow(me, who);
+					}
+					const actualList = await service.packMany(followeeMe, me, { schema: 'UserDetailed' }) as any;
+					for (const actual of actualList) {
+						expect(actual.isFollowing).toBe(true);
+						expect(actual.isFollowed).toBe(false);
+						expect(actual.hasPendingFollowRequestFromYou).toBe(false);
+						expect(actual.hasPendingFollowRequestToYou).toBe(false);
+						expect(actual.isBlocking).toBe(false);
+						expect(actual.isBlocked).toBe(false);
+						expect(actual.isMuted).toBe(false);
+						expect(actual.isRenoteMuted).toBe(false);
+					}
+				}
+
+				{
+					// meをフォローしてる人たち
+					const followerMe = await Promise.all(randomIntRange().map(() => createUser()));
+					for (const who of followerMe) {
+						await follow(who, me);
+					}
+					const actualList = await service.packMany(followerMe, me, { schema: 'UserDetailed' }) as any;
+					for (const actual of actualList) {
+						expect(actual.isFollowing).toBe(false);
+						expect(actual.isFollowed).toBe(true);
+						expect(actual.hasPendingFollowRequestFromYou).toBe(false);
+						expect(actual.hasPendingFollowRequestToYou).toBe(false);
+						expect(actual.isBlocking).toBe(false);
+						expect(actual.isBlocked).toBe(false);
+						expect(actual.isMuted).toBe(false);
+						expect(actual.isRenoteMuted).toBe(false);
+					}
+				}
+
+				{
+					// meがフォローリクエストを送った人たち
+					const requestsFromYou = await Promise.all(randomIntRange().map(() => createUser()));
+					for (const who of requestsFromYou) {
+						await requestFollow(me, who);
+					}
+					const actualList = await service.packMany(requestsFromYou, me, { schema: 'UserDetailed' }) as any;
+					for (const actual of actualList) {
+						expect(actual.isFollowing).toBe(false);
+						expect(actual.isFollowed).toBe(false);
+						expect(actual.hasPendingFollowRequestFromYou).toBe(true);
+						expect(actual.hasPendingFollowRequestToYou).toBe(false);
+						expect(actual.isBlocking).toBe(false);
+						expect(actual.isBlocked).toBe(false);
+						expect(actual.isMuted).toBe(false);
+						expect(actual.isRenoteMuted).toBe(false);
+					}
+				}
+
+				{
+					// meにフォローリクエストを送った人たち
+					const requestsToYou = await Promise.all(randomIntRange().map(() => createUser()));
+					for (const who of requestsToYou) {
+						await requestFollow(who, me);
+					}
+					const actualList = await service.packMany(requestsToYou, me, { schema: 'UserDetailed' }) as any;
+					for (const actual of actualList) {
+						expect(actual.isFollowing).toBe(false);
+						expect(actual.isFollowed).toBe(false);
+						expect(actual.hasPendingFollowRequestFromYou).toBe(false);
+						expect(actual.hasPendingFollowRequestToYou).toBe(true);
+						expect(actual.isBlocking).toBe(false);
+						expect(actual.isBlocked).toBe(false);
+						expect(actual.isMuted).toBe(false);
+						expect(actual.isRenoteMuted).toBe(false);
+					}
+				}
+
+				{
+					// meがブロックしてる人たち
+					const blockingYou = await Promise.all(randomIntRange().map(() => createUser()));
+					for (const who of blockingYou) {
+						await block(me, who);
+					}
+					const actualList = await service.packMany(blockingYou, me, { schema: 'UserDetailed' }) as any;
+					for (const actual of actualList) {
+						expect(actual.isFollowing).toBe(false);
+						expect(actual.isFollowed).toBe(false);
+						expect(actual.hasPendingFollowRequestFromYou).toBe(false);
+						expect(actual.hasPendingFollowRequestToYou).toBe(false);
+						expect(actual.isBlocking).toBe(true);
+						expect(actual.isBlocked).toBe(false);
+						expect(actual.isMuted).toBe(false);
+						expect(actual.isRenoteMuted).toBe(false);
+					}
+				}
+
+				{
+					// meをブロックしてる人たち
+					const blockingMe = await Promise.all(randomIntRange().map(() => createUser()));
+					for (const who of blockingMe) {
+						await block(who, me);
+					}
+					const actualList = await service.packMany(blockingMe, me, { schema: 'UserDetailed' }) as any;
+					for (const actual of actualList) {
+						expect(actual.isFollowing).toBe(false);
+						expect(actual.isFollowed).toBe(false);
+						expect(actual.hasPendingFollowRequestFromYou).toBe(false);
+						expect(actual.hasPendingFollowRequestToYou).toBe(false);
+						expect(actual.isBlocking).toBe(false);
+						expect(actual.isBlocked).toBe(true);
+						expect(actual.isMuted).toBe(false);
+						expect(actual.isRenoteMuted).toBe(false);
+					}
+				}
+
+				{
+					// meがミュートしてる人たち
+					const muters = await Promise.all(randomIntRange().map(() => createUser()));
+					for (const who of muters) {
+						await mute(me, who);
+					}
+					const actualList = await service.packMany(muters, me, { schema: 'UserDetailed' }) as any;
+					for (const actual of actualList) {
+						expect(actual.isFollowing).toBe(false);
+						expect(actual.isFollowed).toBe(false);
+						expect(actual.hasPendingFollowRequestFromYou).toBe(false);
+						expect(actual.hasPendingFollowRequestToYou).toBe(false);
+						expect(actual.isBlocking).toBe(false);
+						expect(actual.isBlocked).toBe(false);
+						expect(actual.isMuted).toBe(true);
+						expect(actual.isRenoteMuted).toBe(false);
+					}
+				}
+
+				{
+					// meがリノートミュートしてる人たち
+					const renoteMuters = await Promise.all(randomIntRange().map(() => createUser()));
+					for (const who of renoteMuters) {
+						await muteRenote(me, who);
+					}
+					const actualList = await service.packMany(renoteMuters, me, { schema: 'UserDetailed' }) as any;
+					for (const actual of actualList) {
+						expect(actual.isFollowing).toBe(false);
+						expect(actual.isFollowed).toBe(false);
+						expect(actual.hasPendingFollowRequestFromYou).toBe(false);
+						expect(actual.hasPendingFollowRequestToYou).toBe(false);
+						expect(actual.isBlocking).toBe(false);
+						expect(actual.isBlocked).toBe(false);
+						expect(actual.isMuted).toBe(false);
+						expect(actual.isRenoteMuted).toBe(true);
+					}
+				}
+			});
+		});
+	});
+});

From 29f6ba6310e162d6e24c9075ddd6176c155a10e7 Mon Sep 17 00:00:00 2001
From: zyoshoka <107108195+zyoshoka@users.noreply.github.com>
Date: Wed, 13 Mar 2024 22:37:18 +0900
Subject: [PATCH 10/14] chore: add missing SPDX ID and workflow check (#13570)

* chore: add workflow which checks if SPDX ID exists

* chore: add missing SPDX ID in some files

* chore: change trigger condition

* chore: trigger on push

* lint
---
 .github/workflows/check-spdx-license-id.yml   | 75 +++++++++++++++++++
 cypress/e2e/basic.cy.js                       |  5 ++
 cypress/e2e/router.cy.js                      |  5 ++
 cypress/e2e/widgets.cy.js                     |  5 ++
 packages/backend/generate_api_json.js         |  7 +-
 .../1689325027964-UserBlacklistAnntena.js     |  5 ++
 .../1690417561185-fix-renote-muting.js        |  5 ++
 ...417561186-ChangeCacheRemoteFilesDefault.js |  5 ++
 .../backend/migration/1690417561187-Fix.js    |  5 ++
 .../1690569881926-user-2fa-backup-codes.js    |  5 ++
 .../1691649257651-refine-announcement.js      |  5 ++
 .../1691657412740-refine-announcement-2.js    |  5 ++
 .../migration/1695260774117-verified-links.js |  5 ++
 .../1695288787870-following-notify.js         |  5 ++
 .../migration/1695440131671-short-name.js     |  5 ++
 .../1695605508898-mutingNotificationTypes.js  |  5 ++
 .../1695901659683-note-updated-at.js          |  5 ++
 .../1696323464251-user-list-membership.js     |  5 ++
 .../migration/1696331570827-hibernation.js    |  5 ++
 .../backend/migration/1696332072038-clean.js  |  5 ++
 .../migration/1700383825690-hard-mute.js      |  5 ++
 .../src/core/ChannelFollowingService.ts       |  5 ++
 .../backend/src/misc/fastify-hook-handlers.ts |  5 ++
 packages/backend/src/misc/is-pure-renote.ts   |  5 ++
 packages/backend/src/misc/loader.ts           |  5 ++
 packages/backend/src/models/ReversiGame.ts    |  5 ++
 .../backend/src/models/json-schema/signin.ts  |  5 ++
 packages/backend/test/jest.setup.ts           |  5 ++
 packages/backend/test/unit/ApMfmService.ts    |  5 ++
 packages/backend/test/unit/misc/loader.ts     |  5 ++
 .../frontend/.storybook/preview-head.html     |  5 ++
 .../frontend/src/components/global/I18n.vue   |  5 ++
 packages/frontend/src/filters/kmg.ts          |  5 ++
 .../src/scripts/check-reaction-permissions.ts |  5 ++
 packages/frontend/src/scripts/clear-cache.ts  |  5 ++
 .../frontend/src/scripts/code-highlighter.ts  |  5 ++
 .../frontend/src/scripts/media-has-audio.ts   |  5 ++
 packages/frontend/src/type.ts                 |  5 ++
 scripts/changelog-checker/src/checker.ts      |  5 ++
 scripts/changelog-checker/src/index.ts        |  5 ++
 scripts/changelog-checker/src/parser.ts       |  5 ++
 .../changelog-checker/test/checker.test.ts    |  7 +-
 scripts/tarball.mjs                           |  5 ++
 43 files changed, 287 insertions(+), 2 deletions(-)
 create mode 100644 .github/workflows/check-spdx-license-id.yml

diff --git a/.github/workflows/check-spdx-license-id.yml b/.github/workflows/check-spdx-license-id.yml
new file mode 100644
index 0000000000..6cd8bf60d5
--- /dev/null
+++ b/.github/workflows/check-spdx-license-id.yml
@@ -0,0 +1,75 @@
+name: Check SPDX-License-Identifier
+
+on:
+  push:
+    branches:
+      - master
+      - develop
+  pull_request:
+
+jobs:
+  check-spdx-license-id:
+    runs-on: ubuntu-latest
+    steps:
+      - name: Checkout
+        uses: actions/checkout@v4.1.1
+      - name: Check
+        run: |
+          counter=0
+
+          search() {
+            local directory="$1"
+            find "$directory" -type f \
+              '(' \
+                -name "*.cjs" -and -not -name '*.config.cjs' -o \
+                -name "*.html" -o \
+                -name "*.js" -and -not -name '*.config.js' -o \
+                -name "*.mjs" -and -not -name '*.config.mjs' -o \
+                -name "*.scss" -o \
+                -name "*.ts" -and -not -name '*.config.ts' -o \
+                -name "*.vue" \
+              ')' -and \
+              -not -name '*eslint*'
+          }
+
+          check() {
+            local file="$1"
+            if ! (
+              grep -q "SPDX-FileCopyrightText: syuilo and misskey-project" "$file" ||
+              grep -q "SPDX-License-Identifier: AGPL-3.0-only" "$file"
+            ); then
+              echo "Missing: $file"
+              ((counter++))
+            fi
+          }
+
+          directories=(
+            "cypress/e2e"
+            "packages/backend/migration"
+            "packages/backend/src"
+            "packages/backend/test"
+            "packages/frontend/.storybook"
+            "packages/frontend/@types"
+            "packages/frontend/lib"
+            "packages/frontend/public"
+            "packages/frontend/src"
+            "packages/frontend/test"
+            "packages/misskey-bubble-game/src"
+            "packages/misskey-reversi/src"
+            "packages/sw/src"
+            "scripts"
+          )
+
+          for directory in "${directories[@]}"; do
+            for file in $(search $directory); do
+              check "$file"
+            done
+          done
+
+          if [ $counter -gt 0 ]; then
+            echo "SPDX-License-Identifier is missing in $counter files."
+            exit 1
+          else
+            echo "SPDX-License-Identifier is certainly described in all target files!"
+            exit 0
+          fi
diff --git a/cypress/e2e/basic.cy.js b/cypress/e2e/basic.cy.js
index 604241d13c..d2525e0a7d 100644
--- a/cypress/e2e/basic.cy.js
+++ b/cypress/e2e/basic.cy.js
@@ -1,3 +1,8 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
 describe('Before setup instance', () => {
 	beforeEach(() => {
 		cy.resetState();
diff --git a/cypress/e2e/router.cy.js b/cypress/e2e/router.cy.js
index 6de27be5f4..8d8fb3af31 100644
--- a/cypress/e2e/router.cy.js
+++ b/cypress/e2e/router.cy.js
@@ -1,3 +1,8 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
 describe('Router transition', () => {
 	describe('Redirect', () => {
 		// サーバの初期化。ルートのテストに関しては各describeごとに1度だけ実行で十分だと思う(使いまわした方が早い)
diff --git a/cypress/e2e/widgets.cy.js b/cypress/e2e/widgets.cy.js
index df6ec8357d..847801a69f 100644
--- a/cypress/e2e/widgets.cy.js
+++ b/cypress/e2e/widgets.cy.js
@@ -1,3 +1,8 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
 /* flaky
 describe('After user signed in', () => {
 	beforeEach(() => {
diff --git a/packages/backend/generate_api_json.js b/packages/backend/generate_api_json.js
index 4079b3bb0a..602ced1d75 100644
--- a/packages/backend/generate_api_json.js
+++ b/packages/backend/generate_api_json.js
@@ -1,3 +1,8 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
 import { loadConfig } from './built/config.js'
 import { genOpenapiSpec } from './built/server/api/openapi/gen-spec.js'
 import { writeFileSync } from "node:fs";
@@ -5,4 +10,4 @@ import { writeFileSync } from "node:fs";
 const config = loadConfig();
 const spec = genOpenapiSpec(config, true);
 
-writeFileSync('./built/api.json', JSON.stringify(spec), 'utf-8');
\ No newline at end of file
+writeFileSync('./built/api.json', JSON.stringify(spec), 'utf-8');
diff --git a/packages/backend/migration/1689325027964-UserBlacklistAnntena.js b/packages/backend/migration/1689325027964-UserBlacklistAnntena.js
index ce246b20f8..2dc7774493 100644
--- a/packages/backend/migration/1689325027964-UserBlacklistAnntena.js
+++ b/packages/backend/migration/1689325027964-UserBlacklistAnntena.js
@@ -1,3 +1,8 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
 export class UserBlacklistAnntena1689325027964 {
     name = 'UserBlacklistAnntena1689325027964'
 
diff --git a/packages/backend/migration/1690417561185-fix-renote-muting.js b/packages/backend/migration/1690417561185-fix-renote-muting.js
index 14150b0362..d9604ca26c 100644
--- a/packages/backend/migration/1690417561185-fix-renote-muting.js
+++ b/packages/backend/migration/1690417561185-fix-renote-muting.js
@@ -1,3 +1,8 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
 export class FixRenoteMuting1690417561185 {
     name = 'FixRenoteMuting1690417561185'
 
diff --git a/packages/backend/migration/1690417561186-ChangeCacheRemoteFilesDefault.js b/packages/backend/migration/1690417561186-ChangeCacheRemoteFilesDefault.js
index 7eda5debe5..9bccdb3bb5 100644
--- a/packages/backend/migration/1690417561186-ChangeCacheRemoteFilesDefault.js
+++ b/packages/backend/migration/1690417561186-ChangeCacheRemoteFilesDefault.js
@@ -1,3 +1,8 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
 export class ChangeCacheRemoteFilesDefault1690417561186 {
     name = 'ChangeCacheRemoteFilesDefault1690417561186'
 
diff --git a/packages/backend/migration/1690417561187-Fix.js b/packages/backend/migration/1690417561187-Fix.js
index e780e66d7b..7f1d62d68c 100644
--- a/packages/backend/migration/1690417561187-Fix.js
+++ b/packages/backend/migration/1690417561187-Fix.js
@@ -1,3 +1,8 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
 export class Fix1690417561187 {
     name = 'Fix1690417561187'
 
diff --git a/packages/backend/migration/1690569881926-user-2fa-backup-codes.js b/packages/backend/migration/1690569881926-user-2fa-backup-codes.js
index 2049df8ea2..a3ef8dcf08 100644
--- a/packages/backend/migration/1690569881926-user-2fa-backup-codes.js
+++ b/packages/backend/migration/1690569881926-user-2fa-backup-codes.js
@@ -1,3 +1,8 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
 export class User2faBackupCodes1690569881926 {
 	name = 'User2faBackupCodes1690569881926'
 
diff --git a/packages/backend/migration/1691649257651-refine-announcement.js b/packages/backend/migration/1691649257651-refine-announcement.js
index d8d63f3103..ac621155d5 100644
--- a/packages/backend/migration/1691649257651-refine-announcement.js
+++ b/packages/backend/migration/1691649257651-refine-announcement.js
@@ -1,3 +1,8 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
 export class RefineAnnouncement1691649257651 {
     name = 'RefineAnnouncement1691649257651'
 
diff --git a/packages/backend/migration/1691657412740-refine-announcement-2.js b/packages/backend/migration/1691657412740-refine-announcement-2.js
index 8791f99f44..67edf19659 100644
--- a/packages/backend/migration/1691657412740-refine-announcement-2.js
+++ b/packages/backend/migration/1691657412740-refine-announcement-2.js
@@ -1,3 +1,8 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
 export class RefineAnnouncement21691657412740 {
     name = 'RefineAnnouncement21691657412740'
 
diff --git a/packages/backend/migration/1695260774117-verified-links.js b/packages/backend/migration/1695260774117-verified-links.js
index 18e0571d81..64c8a9ad8f 100644
--- a/packages/backend/migration/1695260774117-verified-links.js
+++ b/packages/backend/migration/1695260774117-verified-links.js
@@ -1,3 +1,8 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
 export class VerifiedLinks1695260774117 {
     name = 'VerifiedLinks1695260774117'
 
diff --git a/packages/backend/migration/1695288787870-following-notify.js b/packages/backend/migration/1695288787870-following-notify.js
index e7e2194b15..b3f78d5f2a 100644
--- a/packages/backend/migration/1695288787870-following-notify.js
+++ b/packages/backend/migration/1695288787870-following-notify.js
@@ -1,3 +1,8 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
 export class FollowingNotify1695288787870 {
     name = 'FollowingNotify1695288787870'
 
diff --git a/packages/backend/migration/1695440131671-short-name.js b/packages/backend/migration/1695440131671-short-name.js
index 2c37297fc1..fdc256caf8 100644
--- a/packages/backend/migration/1695440131671-short-name.js
+++ b/packages/backend/migration/1695440131671-short-name.js
@@ -1,3 +1,8 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
 export class ShortName1695440131671 {
     name = 'ShortName1695440131671'
 
diff --git a/packages/backend/migration/1695605508898-mutingNotificationTypes.js b/packages/backend/migration/1695605508898-mutingNotificationTypes.js
index 8c0e52a2f0..67d4243142 100644
--- a/packages/backend/migration/1695605508898-mutingNotificationTypes.js
+++ b/packages/backend/migration/1695605508898-mutingNotificationTypes.js
@@ -1,3 +1,8 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
 export class MutingNotificationTypes1695605508898 {
     name = 'MutingNotificationTypes1695605508898'
 
diff --git a/packages/backend/migration/1695901659683-note-updated-at.js b/packages/backend/migration/1695901659683-note-updated-at.js
index d8a151a1f7..e828fb1a6f 100644
--- a/packages/backend/migration/1695901659683-note-updated-at.js
+++ b/packages/backend/migration/1695901659683-note-updated-at.js
@@ -1,3 +1,8 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
 export class NoteUpdatedAt1695901659683 {
     name = 'NoteUpdatedAt1695901659683'
 
diff --git a/packages/backend/migration/1696323464251-user-list-membership.js b/packages/backend/migration/1696323464251-user-list-membership.js
index 7534040c4c..dc1d438dd7 100644
--- a/packages/backend/migration/1696323464251-user-list-membership.js
+++ b/packages/backend/migration/1696323464251-user-list-membership.js
@@ -1,3 +1,8 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
 export class UserListMembership1696323464251 {
     name = 'UserListMembership1696323464251'
 
diff --git a/packages/backend/migration/1696331570827-hibernation.js b/packages/backend/migration/1696331570827-hibernation.js
index 119d35913f..1487ece77c 100644
--- a/packages/backend/migration/1696331570827-hibernation.js
+++ b/packages/backend/migration/1696331570827-hibernation.js
@@ -1,3 +1,8 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
 export class Hibernation1696331570827 {
     name = 'Hibernation1696331570827'
 
diff --git a/packages/backend/migration/1696332072038-clean.js b/packages/backend/migration/1696332072038-clean.js
index 97dba655f4..92a6810d6a 100644
--- a/packages/backend/migration/1696332072038-clean.js
+++ b/packages/backend/migration/1696332072038-clean.js
@@ -1,3 +1,8 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
 export class Clean1696332072038 {
     name = 'Clean1696332072038'
 
diff --git a/packages/backend/migration/1700383825690-hard-mute.js b/packages/backend/migration/1700383825690-hard-mute.js
index afd3247f5c..92c3ada4a1 100644
--- a/packages/backend/migration/1700383825690-hard-mute.js
+++ b/packages/backend/migration/1700383825690-hard-mute.js
@@ -1,3 +1,8 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
 export class HardMute1700383825690 {
     name = 'HardMute1700383825690'
 
diff --git a/packages/backend/src/core/ChannelFollowingService.ts b/packages/backend/src/core/ChannelFollowingService.ts
index 75843b9773..12251595e2 100644
--- a/packages/backend/src/core/ChannelFollowingService.ts
+++ b/packages/backend/src/core/ChannelFollowingService.ts
@@ -1,3 +1,8 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
 import { Inject, Injectable, OnModuleInit } from '@nestjs/common';
 import Redis from 'ioredis';
 import { DI } from '@/di-symbols.js';
diff --git a/packages/backend/src/misc/fastify-hook-handlers.ts b/packages/backend/src/misc/fastify-hook-handlers.ts
index 49a48f6a6b..3e1c099e00 100644
--- a/packages/backend/src/misc/fastify-hook-handlers.ts
+++ b/packages/backend/src/misc/fastify-hook-handlers.ts
@@ -1,3 +1,8 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
 import type { onRequestHookHandler } from 'fastify';
 
 export const handleRequestRedirectToOmitSearch: onRequestHookHandler = (request, reply, done) => {
diff --git a/packages/backend/src/misc/is-pure-renote.ts b/packages/backend/src/misc/is-pure-renote.ts
index 994d981522..f9c2243a06 100644
--- a/packages/backend/src/misc/is-pure-renote.ts
+++ b/packages/backend/src/misc/is-pure-renote.ts
@@ -1,3 +1,8 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
 import type { MiNote } from '@/models/Note.js';
 
 export function isPureRenote(note: MiNote): note is MiNote & { renoteId: NonNullable<MiNote['renoteId']> } {
diff --git a/packages/backend/src/misc/loader.ts b/packages/backend/src/misc/loader.ts
index 25f7b54d31..7f29b9db10 100644
--- a/packages/backend/src/misc/loader.ts
+++ b/packages/backend/src/misc/loader.ts
@@ -1,3 +1,8 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
 export type FetchFunction<K, V> = (key: K) => Promise<V>;
 
 type ResolveReject<V> = Parameters<ConstructorParameters<typeof Promise<V>>[0]>;
diff --git a/packages/backend/src/models/ReversiGame.ts b/packages/backend/src/models/ReversiGame.ts
index c03335dd63..6b29a0ce8c 100644
--- a/packages/backend/src/models/ReversiGame.ts
+++ b/packages/backend/src/models/ReversiGame.ts
@@ -1,3 +1,8 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
 import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
 import { id } from './util/id.js';
 import { MiUser } from './User.js';
diff --git a/packages/backend/src/models/json-schema/signin.ts b/packages/backend/src/models/json-schema/signin.ts
index d27d2490c5..45732a742b 100644
--- a/packages/backend/src/models/json-schema/signin.ts
+++ b/packages/backend/src/models/json-schema/signin.ts
@@ -1,3 +1,8 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
 export const packedSigninSchema = {
 	type: 'object',
 	properties: {
diff --git a/packages/backend/test/jest.setup.ts b/packages/backend/test/jest.setup.ts
index cf5b9bf24d..861bc6db66 100644
--- a/packages/backend/test/jest.setup.ts
+++ b/packages/backend/test/jest.setup.ts
@@ -1,3 +1,8 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
 import { initTestDb, sendEnvResetRequest } from './utils.js';
 
 beforeAll(async () => {
diff --git a/packages/backend/test/unit/ApMfmService.ts b/packages/backend/test/unit/ApMfmService.ts
index 2b79041c86..79cb81f5c9 100644
--- a/packages/backend/test/unit/ApMfmService.ts
+++ b/packages/backend/test/unit/ApMfmService.ts
@@ -1,3 +1,8 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
 import * as assert from 'assert';
 import { Test } from '@nestjs/testing';
 
diff --git a/packages/backend/test/unit/misc/loader.ts b/packages/backend/test/unit/misc/loader.ts
index fa37950951..2cf54e1555 100644
--- a/packages/backend/test/unit/misc/loader.ts
+++ b/packages/backend/test/unit/misc/loader.ts
@@ -1,3 +1,8 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
 import { DebounceLoader } from '@/misc/loader.js';
 
 class Mock {
diff --git a/packages/frontend/.storybook/preview-head.html b/packages/frontend/.storybook/preview-head.html
index 30f3ebfb64..e50c488243 100644
--- a/packages/frontend/.storybook/preview-head.html
+++ b/packages/frontend/.storybook/preview-head.html
@@ -1,3 +1,8 @@
+<!--
+SPDX-FileCopyrightText: syuilo and misskey-project
+SPDX-License-Identifier: AGPL-3.0-only
+-->
+
 <link rel="preload" href="https://github.com/misskey-dev/misskey/blob/master/packages/frontend/assets/about-icon.png?raw=true" as="image" type="image/png" crossorigin="anonymous">
 <link rel="preload" href="https://github.com/misskey-dev/misskey/blob/master/packages/frontend/assets/fedi.jpg?raw=true" as="image" type="image/jpeg" crossorigin="anonymous">
 <link rel="stylesheet" href="https://unpkg.com/@tabler/icons-webfont@2.44.0/tabler-icons.min.css">
diff --git a/packages/frontend/src/components/global/I18n.vue b/packages/frontend/src/components/global/I18n.vue
index 162aa2bcf8..6b7723e6ac 100644
--- a/packages/frontend/src/components/global/I18n.vue
+++ b/packages/frontend/src/components/global/I18n.vue
@@ -1,3 +1,8 @@
+<!--
+SPDX-FileCopyrightText: syuilo and misskey-project
+SPDX-License-Identifier: AGPL-3.0-only
+-->
+
 <template>
 <render/>
 </template>
diff --git a/packages/frontend/src/filters/kmg.ts b/packages/frontend/src/filters/kmg.ts
index 4dcb5c5800..9608e420f6 100644
--- a/packages/frontend/src/filters/kmg.ts
+++ b/packages/frontend/src/filters/kmg.ts
@@ -1,3 +1,8 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
 export default (v, fractionDigits = 0) => {
 	if (v == null) return 'N/A';
 	if (v === 0) return '0';
diff --git a/packages/frontend/src/scripts/check-reaction-permissions.ts b/packages/frontend/src/scripts/check-reaction-permissions.ts
index e7b473dd75..8fc857f84f 100644
--- a/packages/frontend/src/scripts/check-reaction-permissions.ts
+++ b/packages/frontend/src/scripts/check-reaction-permissions.ts
@@ -1,3 +1,8 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
 import * as Misskey from 'misskey-js';
 import { UnicodeEmojiDef } from './emojilist.js';
 
diff --git a/packages/frontend/src/scripts/clear-cache.ts b/packages/frontend/src/scripts/clear-cache.ts
index b20109ec72..71d1232710 100644
--- a/packages/frontend/src/scripts/clear-cache.ts
+++ b/packages/frontend/src/scripts/clear-cache.ts
@@ -1,3 +1,8 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
 import { unisonReload } from '@/scripts/unison-reload.js';
 import * as os from '@/os.js';
 import { miLocalStorage } from '@/local-storage.js';
diff --git a/packages/frontend/src/scripts/code-highlighter.ts b/packages/frontend/src/scripts/code-highlighter.ts
index 2733897bab..5dd0a3be78 100644
--- a/packages/frontend/src/scripts/code-highlighter.ts
+++ b/packages/frontend/src/scripts/code-highlighter.ts
@@ -1,3 +1,8 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
 import { bundledThemesInfo } from 'shiki';
 import { getHighlighterCore, loadWasm } from 'shiki/core';
 import darkPlus from 'shiki/themes/dark-plus.mjs';
diff --git a/packages/frontend/src/scripts/media-has-audio.ts b/packages/frontend/src/scripts/media-has-audio.ts
index 3421a38a76..4bf3ee5d97 100644
--- a/packages/frontend/src/scripts/media-has-audio.ts
+++ b/packages/frontend/src/scripts/media-has-audio.ts
@@ -1,3 +1,8 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
 export default async function hasAudio(media: HTMLMediaElement) {
 	const cloned = media.cloneNode() as HTMLMediaElement;
 	cloned.muted = (cloned as typeof cloned & Partial<HTMLVideoElement>).playsInline = true;
diff --git a/packages/frontend/src/type.ts b/packages/frontend/src/type.ts
index 9c0fc2a11e..5ff27158d2 100644
--- a/packages/frontend/src/type.ts
+++ b/packages/frontend/src/type.ts
@@ -1,3 +1,8 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
 export type WithRequired<T, K extends keyof T> = T & { [P in K]-?: T[P] };
 
 export type WithNonNullable<T, K extends keyof T> = T & { [P in K]-?: NonNullable<T[P]> };
diff --git a/scripts/changelog-checker/src/checker.ts b/scripts/changelog-checker/src/checker.ts
index bbd5b2270a..8fd6ff5629 100644
--- a/scripts/changelog-checker/src/checker.ts
+++ b/scripts/changelog-checker/src/checker.ts
@@ -1,3 +1,8 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
 import { Release } from './parser.js';
 
 export class Result {
diff --git a/scripts/changelog-checker/src/index.ts b/scripts/changelog-checker/src/index.ts
index 8cbeb297d9..0e2c5ce057 100644
--- a/scripts/changelog-checker/src/index.ts
+++ b/scripts/changelog-checker/src/index.ts
@@ -1,3 +1,8 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
 import * as process from 'process';
 import * as fs from 'fs';
 import { parseChangeLog } from './parser.js';
diff --git a/scripts/changelog-checker/src/parser.ts b/scripts/changelog-checker/src/parser.ts
index d6a9ddeda8..894d60d6af 100644
--- a/scripts/changelog-checker/src/parser.ts
+++ b/scripts/changelog-checker/src/parser.ts
@@ -1,3 +1,8 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
 import * as fs from 'node:fs';
 import { unified } from 'unified';
 import remarkParse from 'remark-parse';
diff --git a/scripts/changelog-checker/test/checker.test.ts b/scripts/changelog-checker/test/checker.test.ts
index bc73e5622b..9ca8fa85f2 100644
--- a/scripts/changelog-checker/test/checker.test.ts
+++ b/scripts/changelog-checker/test/checker.test.ts
@@ -1,3 +1,8 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
 import {expect, suite, test} from "vitest";
 import {Release, ReleaseCategory} from "../src/parser";
 import {checkNewRelease, checkNewTopic} from "../src/checker";
@@ -411,4 +416,4 @@ suite('checkNewTopic', () => {
 		console.log(result.message)
 		expect(result.success).toBe(false)
 	})
-})
\ No newline at end of file
+})
diff --git a/scripts/tarball.mjs b/scripts/tarball.mjs
index 936a43d270..b1862ad289 100644
--- a/scripts/tarball.mjs
+++ b/scripts/tarball.mjs
@@ -1,3 +1,8 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
 import { createWriteStream } from 'node:fs';
 import { mkdir } from 'node:fs/promises';
 import { resolve } from 'node:path';

From 88d47ab0245bf5990096e59ced280f62cb7e7a60 Mon Sep 17 00:00:00 2001
From: FineArchs <133759614+FineArchs@users.noreply.github.com>
Date: Wed, 13 Mar 2024 22:38:26 +0900
Subject: [PATCH 11/14] =?UTF-8?q?=E3=83=97=E3=83=A9=E3=82=B0=E3=82=A4?=
 =?UTF-8?q?=E3=83=B3=E3=81=AE=E7=B0=A1=E6=98=93=E7=9A=84=E3=81=AA=E3=83=AD?=
 =?UTF-8?q?=E3=82=B0=E3=82=92=E8=A1=A8=E7=A4=BA=E3=81=99=E3=82=8B=E6=A9=9F?=
 =?UTF-8?q?=E8=83=BD=20(#13564)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

* add plugin logging

* change variable name

* Update plugin.ts

* Update CHANGELOG.md
---
 CHANGELOG.md                                  |  2 ++
 locales/ja-JP.yml                             |  1 +
 .../frontend/src/pages/settings/plugin.vue    | 20 ++++++++++++---
 packages/frontend/src/plugin.ts               | 25 +++++++++++++------
 4 files changed, 37 insertions(+), 11 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 83d0a3f7d2..f1e863a8f2 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -8,6 +8,8 @@
 - Enhance: 広告がMisskeyと同一ドメインの場合はRouterで遷移するように
 - Enhance: リアクション・いいねの総数を表示するように
 - Enhance: リアクション受け入れが「いいねのみ」の場合はリアクション絵文字一覧を表示しないように
+- Enhance: 設定>プラグインのページからプラグインの簡易的なログやエラーを見られるように
+  - 実装の都合により、プラグインは1つエラーを起こした時に即時停止するようになりました
 - Fix: 一部のページ内リンクが正しく動作しない問題を修正
 - Fix: 周年の実績が閏年を考慮しない問題を修正
 - Fix: ローカルURLのプレビューポップアップが左上に表示される
diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml
index 64705868b9..f42fd6587a 100644
--- a/locales/ja-JP.yml
+++ b/locales/ja-JP.yml
@@ -1772,6 +1772,7 @@ _plugin:
   installWarn: "信頼できないプラグインはインストールしないでください。"
   manage: "プラグインの管理"
   viewSource: "ソースを表示"
+  viewLog: "ログを表示"
 
 _preferencesBackups:
   list: "作成したバックアップ"
diff --git a/packages/frontend/src/pages/settings/plugin.vue b/packages/frontend/src/pages/settings/plugin.vue
index 0ab75b95a2..9804454e66 100644
--- a/packages/frontend/src/pages/settings/plugin.vue
+++ b/packages/frontend/src/pages/settings/plugin.vue
@@ -41,13 +41,26 @@ SPDX-License-Identifier: AGPL-3.0-only
 					<MkButton inline danger @click="uninstall(plugin)"><i class="ti ti-trash"></i> {{ i18n.ts.uninstall }}</MkButton>
 				</div>
 
+				<MkFolder>
+					<template #icon><i class="ti ti-terminal-2"></i></template>
+					<template #label>{{ i18n.ts._plugin.viewLog }}</template>
+
+					<div class="_gaps_s">
+						<div class="_buttons">
+							<MkButton inline @click="copy(pluginLogs.get(plugin.id)?.join('\n'))"><i class="ti ti-copy"></i> {{ i18n.ts.copy }}</MkButton>
+						</div>
+
+						<MkCode :code="pluginLogs.get(plugin.id)?.join('\n') ?? ''"/>
+					</div>
+				</MkFolder>
+
 				<MkFolder>
 					<template #icon><i class="ti ti-code"></i></template>
 					<template #label>{{ i18n.ts._plugin.viewSource }}</template>
 
 					<div class="_gaps_s">
 						<div class="_buttons">
-							<MkButton inline @click="copy(plugin)"><i class="ti ti-copy"></i> {{ i18n.ts.copy }}</MkButton>
+							<MkButton inline @click="copy(plugin.src)"><i class="ti ti-copy"></i> {{ i18n.ts.copy }}</MkButton>
 						</div>
 
 						<MkCode :code="plugin.src ?? ''" lang="is"/>
@@ -74,6 +87,7 @@ import { ColdDeviceStorage } from '@/store.js';
 import { unisonReload } from '@/scripts/unison-reload.js';
 import { i18n } from '@/i18n.js';
 import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { pluginLogs } from '@/plugin.js';
 
 const plugins = ref(ColdDeviceStorage.get('plugins'));
 
@@ -87,8 +101,8 @@ async function uninstall(plugin) {
 	});
 }
 
-function copy(plugin) {
-	copyToClipboard(plugin.src ?? '');
+function copy(text) {
+	copyToClipboard(text ?? '');
 	os.success();
 }
 
diff --git a/packages/frontend/src/plugin.ts b/packages/frontend/src/plugin.ts
index 743cadc36a..81233a5a5e 100644
--- a/packages/frontend/src/plugin.ts
+++ b/packages/frontend/src/plugin.ts
@@ -3,6 +3,7 @@
  * SPDX-License-Identifier: AGPL-3.0-only
  */
 
+import { ref } from 'vue';
 import { Interpreter, Parser, utils, values } from '@syuilo/aiscript';
 import { aiScriptReadline, createAiScriptEnv } from '@/scripts/aiscript/api.js';
 import { inputText } from '@/os.js';
@@ -10,6 +11,7 @@ import { Plugin, noteActions, notePostInterruptors, noteViewInterruptors, postFo
 
 const parser = new Parser();
 const pluginContexts = new Map<string, Interpreter>();
+export const pluginLogs = ref(new Map<string, string[]>());
 
 export async function install(plugin: Plugin): Promise<void> {
 	// 後方互換性のため
@@ -22,21 +24,27 @@ export async function install(plugin: Plugin): Promise<void> {
 		in: aiScriptReadline,
 		out: (value): void => {
 			console.log(value);
+			pluginLogs.value.get(plugin.id).push(utils.reprValue(value));
 		},
 		log: (): void => {
 		},
+		err: (err): void => {
+			pluginLogs.value.get(plugin.id).push(`${err}`);
+			throw err; // install時のtry-catchに反応させる
+		},
 	});
 
 	initPlugin({ plugin, aiscript });
 
-	try {
-		await aiscript.exec(parser.parse(plugin.src));
-	} catch (err) {
-		console.error('Plugin install failed:', plugin.name, 'v' + plugin.version);
-		return;
-	}
-
-	console.info('Plugin installed:', plugin.name, 'v' + plugin.version);
+	aiscript.exec(parser.parse(plugin.src)).then(
+		() => {
+			console.info('Plugin installed:', plugin.name, 'v' + plugin.version);
+		},
+		(err) => {
+			console.error('Plugin install failed:', plugin.name, 'v' + plugin.version);
+			throw err;
+		},
+	);
 }
 
 function createPluginEnv(opts: { plugin: Plugin; storageKey: string }): Record<string, values.Value> {
@@ -92,6 +100,7 @@ function createPluginEnv(opts: { plugin: Plugin; storageKey: string }): Record<s
 
 function initPlugin({ plugin, aiscript }): void {
 	pluginContexts.set(plugin.id, aiscript);
+	pluginLogs.value.set(plugin.id, []);
 }
 
 function registerPostFormAction({ pluginId, title, handler }): void {

From 75fa43bc59348c59a6e1a95823b5977bff75d78e Mon Sep 17 00:00:00 2001
From: tamaina <tamaina@hotmail.co.jp>
Date: Thu, 14 Mar 2024 17:39:38 +0900
Subject: [PATCH 12/14] fix(dev): fix duplication in .vscode/extensions.json

---
 .vscode/extensions.json | 1 -
 1 file changed, 1 deletion(-)

diff --git a/.vscode/extensions.json b/.vscode/extensions.json
index d08109477c..3cdf81e339 100644
--- a/.vscode/extensions.json
+++ b/.vscode/extensions.json
@@ -4,7 +4,6 @@
 		"dbaeumer.vscode-eslint",
 		"Vue.volar",
 		"Orta.vscode-jest",
-		"dbaeumer.vscode-eslint",
 		"mrmlnc.vscode-json5"
 	]
 }

From 8604bd98078b3e31d8b53de1f9e54f5dab1f22bd Mon Sep 17 00:00:00 2001
From: tamaina <tamaina@hotmail.co.jp>
Date: Thu, 14 Mar 2024 17:42:30 +0900
Subject: [PATCH 13/14] fix(dev): vscode-jest: Deprecated: Please use
 jest.runMode instead.

---
 .vscode/settings.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/.vscode/settings.json b/.vscode/settings.json
index e2a82b1ffe..0ceec23acd 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -7,7 +7,7 @@
 		"*.test.ts": "typescript"
 	},
 	"jest.jestCommandLine": "pnpm run jest",
-	"jest.autoRun": "off",
+	"jest.runMode": "on-demand",
 	"editor.codeActionsOnSave": {
 		"source.fixAll": "explicit"
 	},

From 71d0538647684d67de1b15467f890bd65ffc770d Mon Sep 17 00:00:00 2001
From: tamaina <tamaina@hotmail.co.jp>
Date: Thu, 14 Mar 2024 18:18:32 +0900
Subject: [PATCH 14/14] fix(frontend): update locales/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 53c3159da6..87065e1d37 100644
--- a/locales/index.d.ts
+++ b/locales/index.d.ts
@@ -6805,6 +6805,10 @@ export interface Locale extends ILocale {
          * ソースを表示
          */
         "viewSource": string;
+        /**
+         * ログを表示
+         */
+        "viewLog": string;
     };
     "_preferencesBackups": {
         /**