From 07d4632cd7839c6a7cdf2e9227a84aefe70ac7fd Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Thu, 21 Dec 2023 14:45:39 +0900
Subject: [PATCH 01/25] Update CHANGELOG.md

Co-authored-by: zyoshoka <107108195+zyoshoka@users.noreply.github.com>
---
 CHANGELOG.md | 1 +
 1 file changed, 1 insertion(+)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index f535aa3d9c..a7135d8745 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -17,6 +17,7 @@
 
 ### Note
 - Node.js 20.10.0が最小要件になりました
+- 絵文字の追加辞書を既にインストールしている場合は、お手数ですが再インストールのほどお願いします
 - 絵文字ピッカーにピン留め表示する絵文字設定が「リアクション用」と「絵文字入力用」に分かれました。以前の設定は「リアクション用」として使用されます。  
 
 	**影響:**  

From 79ca93cefb8c892556b49fe4055d397e2a56adcf Mon Sep 17 00:00:00 2001
From: GrapeApple0 <84321396+GrapeApple0@users.noreply.github.com>
Date: Thu, 21 Dec 2023 16:57:05 +0900
Subject: [PATCH 02/25] =?UTF-8?q?enhance:=20api.json=E3=81=AE=E3=83=AC?=
 =?UTF-8?q?=E3=82=B9=E3=83=9D=E3=83=B3=E3=82=B9=E3=81=AE=E5=86=85=E5=AE=B9?=
 =?UTF-8?q?=E3=82=92=E5=AE=9F=E9=9A=9B=E3=81=AE=E5=86=85=E5=AE=B9=E3=81=AB?=
 =?UTF-8?q?=E5=90=88=E3=82=8F=E3=81=9B=E3=82=8B=20(#12723)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

* Create packedAdSchema

* admin/emoji/add

* admin/get-user-ips

* admin/roles/users

* admin/get-index-stats

* admin/accounts/find-by-email

* fix type of admin/ad/list

* federation/stats

* endpoints

* get-online-users-count

* i/2fa/register-key

* i/2fa/key-done

* i/2fa/register

* i/apps

* i/authorized-apps

* i/registry/get-all

* i/registry/get

* i/registry/get-detail

* i/registry/key-with-type

* i/registry/scopes-with-domain

* i/update-email

* i/move

* i/webhooks/create

* fix miss type

* i/webhooks/show

* i/webhooks/list

* flash/create

* roles/users

* server-info

* test

* users/lists/get-memberships

* users/achievements

* fetch-rss

* fetch-external-resources
---
 packages/backend/src/misc/json-schema.ts      |   2 +
 packages/backend/src/models/json-schema/ad.ts |  64 +++++++++
 .../endpoints/admin/accounts/find-by-email.ts |   5 +
 .../server/api/endpoints/admin/ad/create.ts   |  19 ++-
 .../src/server/api/endpoints/admin/ad/list.ts |  24 +++-
 .../server/api/endpoints/admin/emoji/add.ts   |   2 +
 .../api/endpoints/admin/get-index-stats.ts    |  10 ++
 .../api/endpoints/admin/get-user-ips.ts       |  19 +++
 .../server/api/endpoints/admin/roles/users.ts |  16 ++-
 .../src/server/api/endpoints/endpoint.ts      |  17 +++
 .../server/api/endpoints/federation/stats.ts  |  86 +++++++++++
 .../api/endpoints/fetch-external-resources.ts |  12 ++
 .../src/server/api/endpoints/fetch-rss.ts     |  12 ++
 .../src/server/api/endpoints/flash/create.ts  |   6 +
 .../api/endpoints/get-online-users-count.ts   |  10 ++
 .../server/api/endpoints/i/2fa/key-done.ts    |  10 ++
 .../api/endpoints/i/2fa/register-key.ts       | 134 ++++++++++++++++++
 .../server/api/endpoints/i/2fa/register.ts    |  13 ++
 .../src/server/api/endpoints/i/apps.ts        |  33 ++++-
 .../server/api/endpoints/i/authorized-apps.ts |  30 ++++
 .../src/server/api/endpoints/i/move.ts        |   4 +
 .../api/endpoints/i/registry/get-all.ts       |   4 +
 .../api/endpoints/i/registry/get-detail.ts    |   4 +
 .../server/api/endpoints/i/registry/get.ts    |   4 +
 .../endpoints/i/registry/keys-with-type.ts    |   4 +
 .../i/registry/scopes-with-domain.ts          |  22 +++
 .../server/api/endpoints/i/update-email.ts    |   5 +
 .../server/api/endpoints/i/webhooks/create.ts |  39 ++++-
 .../server/api/endpoints/i/webhooks/list.ts   |  45 +++++-
 .../server/api/endpoints/i/webhooks/show.ts   |  40 +++++-
 .../src/server/api/endpoints/roles/users.ts   |  19 +++
 .../src/server/api/endpoints/server-info.ts   |  47 ++++++
 .../backend/src/server/api/endpoints/test.ts  |  24 ++++
 .../api/endpoints/users/achievements.ts       |  15 ++
 .../endpoints/users/lists/get-memberships.ts  |  29 ++++
 35 files changed, 822 insertions(+), 7 deletions(-)
 create mode 100644 packages/backend/src/models/json-schema/ad.ts

diff --git a/packages/backend/src/misc/json-schema.ts b/packages/backend/src/misc/json-schema.ts
index 49f35b9b74..176978d35f 100644
--- a/packages/backend/src/misc/json-schema.ts
+++ b/packages/backend/src/misc/json-schema.ts
@@ -38,6 +38,7 @@ import { packedFlashSchema } from '@/models/json-schema/flash.js';
 import { packedAnnouncementSchema } from '@/models/json-schema/announcement.js';
 import { packedSigninSchema } from '@/models/json-schema/signin.js';
 import { packedRoleLiteSchema, packedRoleSchema } from '@/models/json-schema/role.js';
+import { packedAdSchema } from '@/models/json-schema/ad.js';
 
 export const refs = {
 	UserLite: packedUserLiteSchema,
@@ -49,6 +50,7 @@ export const refs = {
 	User: packedUserSchema,
 
 	UserList: packedUserListSchema,
+	Ad: packedAdSchema,
 	Announcement: packedAnnouncementSchema,
 	App: packedAppSchema,
 	Note: packedNoteSchema,
diff --git a/packages/backend/src/models/json-schema/ad.ts b/packages/backend/src/models/json-schema/ad.ts
new file mode 100644
index 0000000000..649ffcd4dc
--- /dev/null
+++ b/packages/backend/src/models/json-schema/ad.ts
@@ -0,0 +1,64 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+export const packedAdSchema = {
+	type: 'object',
+	properties: {
+		id: {
+			type: 'string',
+			optional: false,
+			nullable: false,
+			format: 'id',
+			example: 'xxxxxxxxxx',
+		},
+		expiresAt: {
+			type: 'string',
+			optional: false,
+			nullable: false,
+			format: 'date-time',
+		},
+		startsAt: {
+			type: 'string',
+			optional: false,
+			nullable: false,
+			format: 'date-time',
+		},
+		place: {
+			type: 'string',
+			optional: false,
+			nullable: false,
+		},
+		priority: {
+			type: 'string',
+			optional: false,
+			nullable: false,
+		},
+		ratio: {
+			type: 'number',
+			optional: false,
+			nullable: false,
+		},
+		url: {
+			type: 'string',
+			optional: false,
+			nullable: false,
+		},
+		imageUrl: {
+			type: 'string',
+			optional: false,
+			nullable: false,
+		},
+		memo: {
+			type: 'string',
+			optional: false,
+			nullable: false,
+		},
+		dayOfWeek: {
+			type: 'integer',
+			optional: false,
+			nullable: false,
+		},
+	},
+} as const;
diff --git a/packages/backend/src/server/api/endpoints/admin/accounts/find-by-email.ts b/packages/backend/src/server/api/endpoints/admin/accounts/find-by-email.ts
index 7dc9ca830b..bc292fd53a 100644
--- a/packages/backend/src/server/api/endpoints/admin/accounts/find-by-email.ts
+++ b/packages/backend/src/server/api/endpoints/admin/accounts/find-by-email.ts
@@ -25,6 +25,11 @@ export const meta = {
 			id: 'cb865949-8af5-4062-a88c-ef55e8786d1d',
 		},
 	},
+	res: {
+		type: 'object',
+		optional: false, nullable: false,
+		ref: 'User',
+	},
 } as const;
 
 export const paramDef = {
diff --git a/packages/backend/src/server/api/endpoints/admin/ad/create.ts b/packages/backend/src/server/api/endpoints/admin/ad/create.ts
index cbe9727c46..087ae4befc 100644
--- a/packages/backend/src/server/api/endpoints/admin/ad/create.ts
+++ b/packages/backend/src/server/api/endpoints/admin/ad/create.ts
@@ -17,6 +17,12 @@ export const meta = {
 
 	requireCredential: true,
 	requireModerator: true,
+	res: {
+		type: 'object',
+		optional: false,
+		nullable: false,
+		ref: 'Ad',
+	},
 } as const;
 
 export const paramDef = {
@@ -63,7 +69,18 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 				ad: ad,
 			});
 
-			return ad;
+			return {
+				id: ad.id,
+				expiresAt: ad.expiresAt.toISOString(),
+				startsAt: ad.startsAt.toISOString(),
+				dayOfWeek: ad.dayOfWeek,
+				url: ad.url,
+				imageUrl: ad.imageUrl,
+				priority: ad.priority,
+				ratio: ad.ratio,
+				place: ad.place,
+				memo: ad.memo,
+			};
 		});
 	}
 }
diff --git a/packages/backend/src/server/api/endpoints/admin/ad/list.ts b/packages/backend/src/server/api/endpoints/admin/ad/list.ts
index 3bda9fcb02..12528917dc 100644
--- a/packages/backend/src/server/api/endpoints/admin/ad/list.ts
+++ b/packages/backend/src/server/api/endpoints/admin/ad/list.ts
@@ -16,6 +16,17 @@ export const meta = {
 
 	requireCredential: true,
 	requireModerator: true,
+	res: {
+		type: 'array',
+		optional: false,
+		nullable: false,
+		items: {
+			type: 'object',
+			optional: false,
+			nullable: false,
+			ref: 'Ad',
+		},
+	},
 } as const;
 
 export const paramDef = {
@@ -46,7 +57,18 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 			}
 			const ads = await query.limit(ps.limit).getMany();
 
-			return ads;
+			return ads.map(ad => ({
+				id: ad.id,
+				expiresAt: ad.expiresAt.toISOString(),
+				startsAt: ad.startsAt.toISOString(),
+				dayOfWeek: ad.dayOfWeek,
+				url: ad.url,
+				imageUrl: ad.imageUrl,
+				memo: ad.memo,
+				place: ad.place,
+				priority: ad.priority,
+				ratio: ad.ratio,
+			}));
 		});
 	}
 }
diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/add.ts b/packages/backend/src/server/api/endpoints/admin/emoji/add.ts
index 360926594a..76ff1c6b94 100644
--- a/packages/backend/src/server/api/endpoints/admin/emoji/add.ts
+++ b/packages/backend/src/server/api/endpoints/admin/emoji/add.ts
@@ -31,6 +31,8 @@ export const meta = {
 			id: 'f7a3462c-4e6e-4069-8421-b9bd4f4c3975',
 		},
 	},
+
+	ref: 'EmojiDetailed',
 } as const;
 
 export const paramDef = {
diff --git a/packages/backend/src/server/api/endpoints/admin/get-index-stats.ts b/packages/backend/src/server/api/endpoints/admin/get-index-stats.ts
index 2de85f655a..b81d9857d7 100644
--- a/packages/backend/src/server/api/endpoints/admin/get-index-stats.ts
+++ b/packages/backend/src/server/api/endpoints/admin/get-index-stats.ts
@@ -15,6 +15,16 @@ export const meta = {
 	kind: 'read:admin',
 
 	tags: ['admin'],
+	res: {
+		type: 'array',
+		items: {
+			type: 'object',
+			properties: {
+				tablename: { type: 'string' },
+				indexname: { type: 'string' },
+			},
+		},
+	},
 } as const;
 
 export const paramDef = {
diff --git a/packages/backend/src/server/api/endpoints/admin/get-user-ips.ts b/packages/backend/src/server/api/endpoints/admin/get-user-ips.ts
index 6a404c0c77..76c32f2a9f 100644
--- a/packages/backend/src/server/api/endpoints/admin/get-user-ips.ts
+++ b/packages/backend/src/server/api/endpoints/admin/get-user-ips.ts
@@ -16,6 +16,25 @@ export const meta = {
 
 	requireCredential: true,
 	requireModerator: true,
+	res: {
+		type: 'array',
+		optional: false,
+		nullable: false,
+		items: {
+			type: 'object',
+			optional: false,
+			nullable: false,
+			properties: {
+				ip: { type: 'string' },
+				createdAt: {
+					type: 'string',
+					optional: false,
+					nullable: false,
+					format: 'date-time',
+				},
+			},
+		},
+	}
 } as const;
 
 export const paramDef = {
diff --git a/packages/backend/src/server/api/endpoints/admin/roles/users.ts b/packages/backend/src/server/api/endpoints/admin/roles/users.ts
index 53145a32d6..6a0f7f9987 100644
--- a/packages/backend/src/server/api/endpoints/admin/roles/users.ts
+++ b/packages/backend/src/server/api/endpoints/admin/roles/users.ts
@@ -28,6 +28,20 @@ export const meta = {
 			id: '224eff5e-2488-4b18-b3e7-f50d94421648',
 		},
 	},
+
+	res: {
+		type: 'array',
+		items: {
+			type: 'object',
+			properties: {
+				id: { type: 'string', format: 'misskey:id' },
+				createdAt: { type: 'string', format: 'date-time' },
+				user: { ref: 'UserDetailed' },
+				expiresAt: { type: 'string', format: 'date-time', nullable: true },
+			},
+			required: ['id', 'createdAt', 'user'],
+		},
+	}
 } as const;
 
 export const paramDef = {
@@ -80,7 +94,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 				id: assign.id,
 				createdAt: this.idService.parse(assign.id).date.toISOString(),
 				user: await this.userEntityService.pack(assign.user!, me, { detail: true }),
-				expiresAt: assign.expiresAt,
+				expiresAt: assign.expiresAt?.toISOString() ?? null,
 			})));
 		});
 	}
diff --git a/packages/backend/src/server/api/endpoints/endpoint.ts b/packages/backend/src/server/api/endpoints/endpoint.ts
index cecaded20a..66ac8f664f 100644
--- a/packages/backend/src/server/api/endpoints/endpoint.ts
+++ b/packages/backend/src/server/api/endpoints/endpoint.ts
@@ -11,6 +11,23 @@ export const meta = {
 	requireCredential: false,
 
 	tags: ['meta'],
+
+	res: {
+		type: 'object',
+		nullable: true,
+		properties: {
+			params: {
+				type: 'array',
+				items: {
+					type: 'object',
+					properties: {
+						name: { type: 'string' },
+						type: { type: 'string' },
+					},
+				},
+			},
+		},
+	},
 } as const;
 
 export const paramDef = {
diff --git a/packages/backend/src/server/api/endpoints/federation/stats.ts b/packages/backend/src/server/api/endpoints/federation/stats.ts
index e3ffea7b7e..6548142d41 100644
--- a/packages/backend/src/server/api/endpoints/federation/stats.ts
+++ b/packages/backend/src/server/api/endpoints/federation/stats.ts
@@ -18,6 +18,92 @@ export const meta = {
 
 	allowGet: true,
 	cacheSec: 60 * 60,
+
+	res: {
+		type: 'object',
+		optional: false,
+		nullable: false,
+		properties: {
+			topSubInstances: {
+				type: 'array',
+				optional: false,
+				nullable: false,
+				items: {
+					properties: {
+						id: { type: 'string' },
+						firstRetrievedAt: { type: 'string' },
+						host: { type: 'string' },
+						usersCount: { type: 'number' },
+						notesCount: { type: 'number' },
+						followingCount: { type: 'number' },
+						followersCount: { type: 'number' },
+						isNotResponding: { type: 'boolean' },
+						isSuspended: { type: 'boolean' },
+						isBlocked: { type: 'boolean' },
+						softwareName: { type: 'string' },
+						softwareVersion: { type: 'string' },
+						openRegistrations: { type: 'boolean' },
+						name: { type: 'string' },
+						description: { type: 'string' },
+						maintainerName: { type: 'string' },
+						maintainerEmail: { type: 'string' },
+						isSilenced: { type: 'boolean' },
+						iconUrl: { type: 'string' },
+						faviconUrl: { type: 'string' },
+						themeColor: { type: 'string' },
+						infoUpdatedAt: {
+							type: 'string',
+							nullable: true,
+						},
+						latestRequestReceivedAt: {
+							type: 'string',
+							nullable: true,
+						},
+					}
+				},
+			},
+			otherFollowersCount: { type: 'number' },
+			topPubInstances: {
+				type: 'array',
+				optional: false,
+				nullable: false,
+				items: {
+					properties: {
+						id: { type: 'string' },
+						firstRetrievedAt: { type: 'string' },
+						host: { type: 'string' },
+						usersCount: { type: 'number' },
+						notesCount: { type: 'number' },
+						followingCount: { type: 'number' },
+						followersCount: { type: 'number' },
+						isNotResponding: { type: 'boolean' },
+						isSuspended: { type: 'boolean' },
+						isBlocked: { type: 'boolean' },
+						softwareName: { type: 'string' },
+						softwareVersion: { type: 'string' },
+						openRegistrations: { type: 'boolean' },
+						name: { type: 'string' },
+						description: { type: 'string' },
+						maintainerName: { type: 'string' },
+						maintainerEmail: { type: 'string' },
+						isSilenced: { type: 'boolean' },
+						iconUrl: { type: 'string' },
+						faviconUrl: { type: 'string' },
+						themeColor: { type: 'string' },
+						infoUpdatedAt: {
+							type: 'string',
+							nullable: true,
+						},
+						latestRequestReceivedAt: {
+							type: 'string',
+							nullable: true,
+						},
+					}
+				},
+			},
+			otherFollowingCount: { type: 'number' },
+		},
+	}
 } as const;
 
 export const paramDef = {
diff --git a/packages/backend/src/server/api/endpoints/fetch-external-resources.ts b/packages/backend/src/server/api/endpoints/fetch-external-resources.ts
index d7b46cc666..6391a2f580 100644
--- a/packages/backend/src/server/api/endpoints/fetch-external-resources.ts
+++ b/packages/backend/src/server/api/endpoints/fetch-external-resources.ts
@@ -32,6 +32,18 @@ export const meta = {
 			id: '693ba8ba-b486-40df-a174-72f8279b56a4',
 		},
 	},
+
+	res: {
+		type: 'object',
+		properties: {
+			type: {
+				type: 'string',
+			},
+			data: {
+				type: 'string',
+			},
+		},
+	},
 } as const;
 
 export const paramDef = {
diff --git a/packages/backend/src/server/api/endpoints/fetch-rss.ts b/packages/backend/src/server/api/endpoints/fetch-rss.ts
index 37859d8330..b2dee83fe9 100644
--- a/packages/backend/src/server/api/endpoints/fetch-rss.ts
+++ b/packages/backend/src/server/api/endpoints/fetch-rss.ts
@@ -16,6 +16,18 @@ export const meta = {
 	requireCredential: false,
 	allowGet: true,
 	cacheSec: 60 * 3,
+
+	res: {
+		type: 'object',
+		properties: {
+			items: {
+				type: 'array',
+				items: {
+					type: 'object',
+				},
+			}
+		}
+	},
 } as const;
 
 export const paramDef = {
diff --git a/packages/backend/src/server/api/endpoints/flash/create.ts b/packages/backend/src/server/api/endpoints/flash/create.ts
index 4fa65ac9aa..674f323734 100644
--- a/packages/backend/src/server/api/endpoints/flash/create.ts
+++ b/packages/backend/src/server/api/endpoints/flash/create.ts
@@ -27,6 +27,12 @@ export const meta = {
 
 	errors: {
 	},
+
+	res: {
+		type: 'object',
+		optional: false, nullable: false,
+		ref: 'Flash',
+	},
 } as const;
 
 export const paramDef = {
diff --git a/packages/backend/src/server/api/endpoints/get-online-users-count.ts b/packages/backend/src/server/api/endpoints/get-online-users-count.ts
index 8a61168f25..737d637b7e 100644
--- a/packages/backend/src/server/api/endpoints/get-online-users-count.ts
+++ b/packages/backend/src/server/api/endpoints/get-online-users-count.ts
@@ -16,6 +16,16 @@ export const meta = {
 	requireCredential: false,
 	allowGet: true,
 	cacheSec: 60 * 1,
+	res: {
+		type: 'object',
+		optional: false, nullable: false,
+		properties: {
+			count: {
+				type: 'number',
+				nullable: false,
+			},
+		},
+	},
 } as const;
 
 export const paramDef = {
diff --git a/packages/backend/src/server/api/endpoints/i/2fa/key-done.ts b/packages/backend/src/server/api/endpoints/i/2fa/key-done.ts
index 6d530aba3b..a7be47fd0f 100644
--- a/packages/backend/src/server/api/endpoints/i/2fa/key-done.ts
+++ b/packages/backend/src/server/api/endpoints/i/2fa/key-done.ts
@@ -32,6 +32,16 @@ export const meta = {
 			id: '798d6847-b1ed-4f9c-b1f9-163c42655995',
 		},
 	},
+
+	res: {
+		type: 'object',
+		nullable: false,
+		optional: false,
+		properties: {
+			id: { type: 'string' },
+			name: { type: 'string' },
+		},
+	},
 } as const;
 
 export const paramDef = {
diff --git a/packages/backend/src/server/api/endpoints/i/2fa/register-key.ts b/packages/backend/src/server/api/endpoints/i/2fa/register-key.ts
index c39005f2dd..0fac96d58f 100644
--- a/packages/backend/src/server/api/endpoints/i/2fa/register-key.ts
+++ b/packages/backend/src/server/api/endpoints/i/2fa/register-key.ts
@@ -36,6 +36,140 @@ export const meta = {
 			id: 'bf32b864-449b-47b8-974e-f9a5468546f1',
 		},
 	},
+
+	res: {
+		type: 'object',
+		nullable: false,
+		optional: false,
+		properties: {
+			rp: {
+				type: 'object',
+				properties: {
+					id: {
+						type: 'string',
+						nullable: true,
+					},
+				},
+			},
+			user: {
+				type: 'object',
+				properties: {
+					id: {
+						type: 'string',
+					},
+					name: {
+						type: 'string',
+					},
+					displayName: {
+						type: 'string',
+					},
+				},
+			},
+			challenge: {
+				type: 'string',
+			},
+			pubKeyCredParams: {
+				type: 'array',
+				items: {
+					type: 'object',
+					properties: {
+						type: {
+							type: 'string',
+						},
+						alg: {
+							type: 'number',
+						},
+					},
+				},
+			},
+			timeout: {
+				type: 'number',
+				nullable: true,
+			},
+			excludeCredentials: {
+				type: 'array',
+				nullable: true,
+				items: {
+					type: 'object',
+					properties: {
+						id: {
+							type: 'string',
+						},
+						type: {
+							type: 'string',
+						},
+						transports: {
+							type: 'array',
+							items: {
+								type: 'string',
+								enum: [
+									"ble",
+									"cable",
+									"hybrid",
+									"internal",
+									"nfc",
+									"smart-card",
+									"usb",
+								],
+							},
+						},
+					},
+				},
+			},
+			authenticatorSelection: {
+				type: 'object',
+				nullable: true,
+				properties: {
+					authenticatorAttachment: {
+						type: 'string',
+						enum: [
+							"cross-platform",
+							"platform",
+						],
+					},
+					requireResidentKey: {
+						type: 'boolean',
+					},
+					userVerification: {
+						type: 'string',
+						enum: [
+							"discouraged",
+							"preferred",
+							"required",
+						],
+					},
+				},
+			},
+			attestation: {
+				type: 'string',
+				nullable: true,
+				enum: [
+					"direct",
+					"enterprise",
+					"indirect",
+					"none",
+				],
+			},
+			extensions: {
+				type: 'object',
+				nullable: true,
+				properties: {
+					appid: {
+						type: 'string',
+						nullable: true,
+					},
+					credProps: {
+						type: 'boolean',
+						nullable: true,
+					},
+					hmacCreateSecret: {
+						type: 'boolean',
+						nullable: true,
+					},
+				},
+			},
+		},
+	},
 } as const;
 
 export const paramDef = {
diff --git a/packages/backend/src/server/api/endpoints/i/2fa/register.ts b/packages/backend/src/server/api/endpoints/i/2fa/register.ts
index b358c812ee..cc083cbf7b 100644
--- a/packages/backend/src/server/api/endpoints/i/2fa/register.ts
+++ b/packages/backend/src/server/api/endpoints/i/2fa/register.ts
@@ -26,6 +26,19 @@ export const meta = {
 			id: '78d6c839-20c9-4c66-b90a-fc0542168b48',
 		},
 	},
+
+	res: {
+		type: 'object',
+		nullable: false,
+		optional: false,
+		properties: {
+			qr: { type: 'string' },
+			url: { type: 'string' },
+			secret: { type: 'string' },
+			label: { type: 'string' },
+			issuer: { type: 'string' },
+		},
+	},
 } as const;
 
 export const paramDef = {
diff --git a/packages/backend/src/server/api/endpoints/i/apps.ts b/packages/backend/src/server/api/endpoints/i/apps.ts
index 09f6540a77..ef89f93181 100644
--- a/packages/backend/src/server/api/endpoints/i/apps.ts
+++ b/packages/backend/src/server/api/endpoints/i/apps.ts
@@ -13,6 +13,37 @@ export const meta = {
 	requireCredential: true,
 
 	secure: true,
+
+	res: {
+		type: 'array',
+		items: {
+			type: 'object',
+			properties: {
+				id: {
+					type: 'string',
+					format: 'misskey:id',
+				},
+				name: {
+					type: 'string',
+				},
+				createdAt: {
+					type: 'string',
+					format: 'date-time',
+				},
+				lastUsedAt: {
+					type: 'string',
+					format: 'date-time',
+				},
+				permission: {
+					type: 'array',
+					uniqueItems: true,
+					items: {
+						type: 'string'
+					},
+				}
+			},
+		},
+	},
 } as const;
 
 export const paramDef = {
@@ -50,7 +81,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 				id: token.id,
 				name: token.name ?? token.app?.name,
 				createdAt: this.idService.parse(token.id).date.toISOString(),
-				lastUsedAt: token.lastUsedAt,
+				lastUsedAt: token.lastUsedAt?.toISOString(),
 				permission: token.permission,
 			})));
 		});
diff --git a/packages/backend/src/server/api/endpoints/i/authorized-apps.ts b/packages/backend/src/server/api/endpoints/i/authorized-apps.ts
index 32061c2aa4..a0ed371fb8 100644
--- a/packages/backend/src/server/api/endpoints/i/authorized-apps.ts
+++ b/packages/backend/src/server/api/endpoints/i/authorized-apps.ts
@@ -14,6 +14,36 @@ export const meta = {
 	requireCredential: true,
 
 	secure: true,
+
+	res: {
+		type: 'array',
+		items: {
+			type: 'object',
+			properties: {
+				id: {
+					type: 'string',
+					format: 'misskey:id',
+				},
+				name: {
+					type: 'string',
+				},
+				callbackUrl: {
+					type: 'string',
+					nullable: true,
+				},
+				permission: {
+					type: 'array',
+					uniqueItems: true,
+					items: {
+						type: 'string'
+					},
+				},
+				isAuthorized: {
+					type: 'boolean',
+				},
+			},
+		},
+	},
 } as const;
 
 export const paramDef = {
diff --git a/packages/backend/src/server/api/endpoints/i/move.ts b/packages/backend/src/server/api/endpoints/i/move.ts
index 86b726e054..f3ba720c2b 100644
--- a/packages/backend/src/server/api/endpoints/i/move.ts
+++ b/packages/backend/src/server/api/endpoints/i/move.ts
@@ -64,6 +64,10 @@ export const meta = {
 			id: 'b234a14e-9ebe-4581-8000-074b3c215962',
 		},
 	},
+
+	res: {
+		type: 'object',
+	},
 } as const;
 
 export const paramDef = {
diff --git a/packages/backend/src/server/api/endpoints/i/registry/get-all.ts b/packages/backend/src/server/api/endpoints/i/registry/get-all.ts
index 29fa0a29cc..bd6e85a074 100644
--- a/packages/backend/src/server/api/endpoints/i/registry/get-all.ts
+++ b/packages/backend/src/server/api/endpoints/i/registry/get-all.ts
@@ -9,6 +9,10 @@ import { RegistryApiService } from '@/core/RegistryApiService.js';
 
 export const meta = {
 	requireCredential: true,
+
+	res: {
+		type: 'object',
+	},
 } as const;
 
 export const paramDef = {
diff --git a/packages/backend/src/server/api/endpoints/i/registry/get-detail.ts b/packages/backend/src/server/api/endpoints/i/registry/get-detail.ts
index 5b460b45d6..2352beb130 100644
--- a/packages/backend/src/server/api/endpoints/i/registry/get-detail.ts
+++ b/packages/backend/src/server/api/endpoints/i/registry/get-detail.ts
@@ -18,6 +18,10 @@ export const meta = {
 			id: '97a1e8e7-c0f7-47d2-957a-92e61256e01a',
 		},
 	},
+
+	res: {
+		type: 'object',
+	}
 } as const;
 
 export const paramDef = {
diff --git a/packages/backend/src/server/api/endpoints/i/registry/get.ts b/packages/backend/src/server/api/endpoints/i/registry/get.ts
index e8c28298ef..4155a43e0d 100644
--- a/packages/backend/src/server/api/endpoints/i/registry/get.ts
+++ b/packages/backend/src/server/api/endpoints/i/registry/get.ts
@@ -18,6 +18,10 @@ export const meta = {
 			id: 'ac3ed68a-62f0-422b-a7bc-d5e09e8f6a6a',
 		},
 	},
+
+	res: {
+		type: 'object',
+	}
 } as const;
 
 export const paramDef = {
diff --git a/packages/backend/src/server/api/endpoints/i/registry/keys-with-type.ts b/packages/backend/src/server/api/endpoints/i/registry/keys-with-type.ts
index 8953ee5d3d..b411cdd3d9 100644
--- a/packages/backend/src/server/api/endpoints/i/registry/keys-with-type.ts
+++ b/packages/backend/src/server/api/endpoints/i/registry/keys-with-type.ts
@@ -9,6 +9,10 @@ import { RegistryApiService } from '@/core/RegistryApiService.js';
 
 export const meta = {
 	requireCredential: true,
+
+	res: {
+		type: 'object',
+	},
 } as const;
 
 export const paramDef = {
diff --git a/packages/backend/src/server/api/endpoints/i/registry/scopes-with-domain.ts b/packages/backend/src/server/api/endpoints/i/registry/scopes-with-domain.ts
index 1ff994b82c..0aca2a26fe 100644
--- a/packages/backend/src/server/api/endpoints/i/registry/scopes-with-domain.ts
+++ b/packages/backend/src/server/api/endpoints/i/registry/scopes-with-domain.ts
@@ -10,6 +10,28 @@ import { RegistryApiService } from '@/core/RegistryApiService.js';
 export const meta = {
 	requireCredential: true,
 	secure: true,
+
+	res: {
+		type: 'array',
+		items: {
+			type: 'object',
+			properties: {
+				scopes: {
+					type: 'array',
+					items: {
+						type: 'array',
+						items: {
+							type: 'string',
+						}
+					}
+				},
+				domain: {
+					type: 'string',
+					nullable: true,
+				},
+			},
+		},
+	}
 } as const;
 
 export const paramDef = {
diff --git a/packages/backend/src/server/api/endpoints/i/update-email.ts b/packages/backend/src/server/api/endpoints/i/update-email.ts
index a36b3a732b..52977f5a07 100644
--- a/packages/backend/src/server/api/endpoints/i/update-email.ts
+++ b/packages/backend/src/server/api/endpoints/i/update-email.ts
@@ -40,6 +40,11 @@ export const meta = {
 			id: 'a2defefb-f220-8849-0af6-17f816099323',
 		},
 	},
+
+	res: {
+		type: 'object',
+		ref: 'UserDetailed',
+	},
 } as const;
 
 export const paramDef = {
diff --git a/packages/backend/src/server/api/endpoints/i/webhooks/create.ts b/packages/backend/src/server/api/endpoints/i/webhooks/create.ts
index f00dba4a85..bdc9f9ea8b 100644
--- a/packages/backend/src/server/api/endpoints/i/webhooks/create.ts
+++ b/packages/backend/src/server/api/endpoints/i/webhooks/create.ts
@@ -27,6 +27,33 @@ export const meta = {
 			id: '87a9bb19-111e-4e37-81d3-a3e7426453b0',
 		},
 	},
+
+	res: {
+		type: 'object',
+		properties: {
+			id: {
+				type: 'string',
+				format: 'misskey:id'
+			},
+			userId: {
+				type: 'string',
+				format: 'misskey:id',
+			},
+			name: { type: 'string' },
+			on: {
+				type: 'array',
+				items: {
+					type: 'string',
+					enum: webhookEventTypes,
+				}
+			},
+			url: { type: 'string' },
+			secret: { type: 'string' },
+			active: { type: 'boolean' },
+			latestSentAt: { type: 'string', format: 'date-time', nullable: true },
+			latestStatus: { type: 'integer', nullable: true },
+		},
+	},
 } as const;
 
 export const paramDef = {
@@ -73,7 +100,17 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 
 			this.globalEventService.publishInternalEvent('webhookCreated', webhook);
 
-			return webhook;
+			return {
+				id: webhook.id,
+				userId: webhook.userId,
+				name: webhook.name,
+				on: webhook.on,
+				url: webhook.url,
+				secret: webhook.secret,
+				active: webhook.active,
+				latestSentAt: webhook.latestSentAt?.toISOString(),
+				latestStatus: webhook.latestStatus,
+			};
 		});
 	}
 }
diff --git a/packages/backend/src/server/api/endpoints/i/webhooks/list.ts b/packages/backend/src/server/api/endpoints/i/webhooks/list.ts
index aa8921fe24..afb2d0509e 100644
--- a/packages/backend/src/server/api/endpoints/i/webhooks/list.ts
+++ b/packages/backend/src/server/api/endpoints/i/webhooks/list.ts
@@ -5,6 +5,7 @@
 
 import { Inject, Injectable } from '@nestjs/common';
 import { Endpoint } from '@/server/api/endpoint-base.js';
+import { webhookEventTypes } from '@/models/Webhook.js';
 import type { WebhooksRepository } from '@/models/_.js';
 import { DI } from '@/di-symbols.js';
 
@@ -14,6 +15,36 @@ export const meta = {
 	requireCredential: true,
 
 	kind: 'read:account',
+
+	res: {
+		type: 'array',
+		items: {
+			type: 'object',
+			properties: {
+				id: {
+					type: 'string',
+					format: 'misskey:id'
+				},
+				userId: {
+					type: 'string',
+					format: 'misskey:id',
+				},
+				name: { type: 'string' },
+				on: {
+					type: 'array',
+					items: {
+						type: 'string',
+						enum: webhookEventTypes,
+					}
+				},
+				url: { type: 'string' },
+				secret: { type: 'string' },
+				active: { type: 'boolean' },
+				latestSentAt: { type: 'string', format: 'date-time', nullable: true },
+				latestStatus: { type: 'integer', nullable: true },
+			},
+		}
+	}
 } as const;
 
 export const paramDef = {
@@ -33,7 +64,19 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 				userId: me.id,
 			});
 
-			return webhooks;
+			return webhooks.map(webhook => (
+				{
+					id: webhook.id,
+					userId: webhook.userId,
+					name: webhook.name,
+					on: webhook.on,
+					url: webhook.url,
+					secret: webhook.secret,
+					active: webhook.active,
+					latestSentAt: webhook.latestSentAt?.toISOString(),
+					latestStatus: webhook.latestStatus,
+				}
+			));
 		});
 	}
 }
diff --git a/packages/backend/src/server/api/endpoints/i/webhooks/show.ts b/packages/backend/src/server/api/endpoints/i/webhooks/show.ts
index f1294bb5c8..5c6dd908b4 100644
--- a/packages/backend/src/server/api/endpoints/i/webhooks/show.ts
+++ b/packages/backend/src/server/api/endpoints/i/webhooks/show.ts
@@ -5,6 +5,7 @@
 
 import { Inject, Injectable } from '@nestjs/common';
 import { Endpoint } from '@/server/api/endpoint-base.js';
+import { webhookEventTypes } from '@/models/Webhook.js';
 import type { WebhooksRepository } from '@/models/_.js';
 import { DI } from '@/di-symbols.js';
 import { ApiError } from '../../../error.js';
@@ -23,6 +24,33 @@ export const meta = {
 			id: '50f614d9-3047-4f7e-90d8-ad6b2d5fb098',
 		},
 	},
+
+	res: {
+		type: 'object',
+		properties: {
+			id: {
+				type: 'string',
+				format: 'misskey:id'
+			},
+			userId: {
+				type: 'string',
+				format: 'misskey:id',
+			},
+			name: { type: 'string' },
+			on: {
+				type: 'array',
+				items: {
+					type: 'string',
+					enum: webhookEventTypes,
+				}
+			},
+			url: { type: 'string' },
+			secret: { type: 'string' },
+			active: { type: 'boolean' },
+			latestSentAt: { type: 'string', format: 'date-time', nullable: true },
+			latestStatus: { type: 'integer', nullable: true },
+		},
+	},
 } as const;
 
 export const paramDef = {
@@ -49,7 +77,17 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 				throw new ApiError(meta.errors.noSuchWebhook);
 			}
 
-			return webhook;
+			return {
+				id: webhook.id,
+				userId: webhook.userId,
+				name: webhook.name,
+				on: webhook.on,
+				url: webhook.url,
+				secret: webhook.secret,
+				active: webhook.active,
+				latestSentAt: webhook.latestSentAt?.toISOString(),
+				latestStatus: webhook.latestStatus,
+			};
 		});
 	}
 }
diff --git a/packages/backend/src/server/api/endpoints/roles/users.ts b/packages/backend/src/server/api/endpoints/roles/users.ts
index caaa3735e9..d304d075b2 100644
--- a/packages/backend/src/server/api/endpoints/roles/users.ts
+++ b/packages/backend/src/server/api/endpoints/roles/users.ts
@@ -24,6 +24,25 @@ export const meta = {
 			id: '30aaaee3-4792-48dc-ab0d-cf501a575ac5',
 		},
 	},
+
+	res: {
+		type: 'array',
+		items: {
+			type: 'object',
+			nullable: false,
+			properties: {
+				id: {
+					type: 'string',
+					format: 'misskey:id'
+				},
+				user: {
+					type: 'object',
+					ref: 'User'
+				},
+			},
+			required: ['id', 'user'],
+		},
+	},
 } as const;
 
 export const paramDef = {
diff --git a/packages/backend/src/server/api/endpoints/server-info.ts b/packages/backend/src/server/api/endpoints/server-info.ts
index c8cb63e6b3..079f2d7f1d 100644
--- a/packages/backend/src/server/api/endpoints/server-info.ts
+++ b/packages/backend/src/server/api/endpoints/server-info.ts
@@ -15,6 +15,53 @@ export const meta = {
 	cacheSec: 60 * 1,
 
 	tags: ['meta'],
+	res: {
+		type: 'object',
+		optional: false, nullable: false,
+		properties: {
+			machine: {
+				type: 'string',
+				nullable: false,
+			},
+			cpu: {
+				type: 'object',
+				nullable: false,
+				properties: {
+					model: {
+						type: 'string',
+						nullable: false,
+					},
+					cores: {
+						type: 'number',
+						nullable: false,
+					},
+				},
+			},
+			mem: {
+				type: 'object',
+				properties: {
+					total: {
+						type: 'number',
+						nullable: false,
+					},
+				},
+			},
+			fs: {
+				type: 'object',
+				nullable: false,
+				properties: {
+					total: {
+						type: 'number',
+						nullable: false,
+					},
+					used: {
+						type: 'number',
+						nullable: false,
+					},
+				},
+			},
+		},
+	},
 } as const;
 
 export const paramDef = {
diff --git a/packages/backend/src/server/api/endpoints/test.ts b/packages/backend/src/server/api/endpoints/test.ts
index 6d6d44f752..949867c572 100644
--- a/packages/backend/src/server/api/endpoints/test.ts
+++ b/packages/backend/src/server/api/endpoints/test.ts
@@ -12,6 +12,30 @@ export const meta = {
 	description: 'Endpoint for testing input validation.',
 
 	requireCredential: false,
+
+	res: {
+		type: 'object',
+		properties: {
+			id: {
+				type: 'string',
+				format: 'misskey:id'
+			},
+			required: {
+				type: 'boolean',
+			},
+			string: {
+				type: 'string',
+			},
+			default: {
+				type: 'string',
+			},
+			nullableDefault: {
+				type: 'string',
+				default: 'hello',
+				nullable: true,
+			},
+		}
+	}
 } as const;
 
 export const paramDef = {
diff --git a/packages/backend/src/server/api/endpoints/users/achievements.ts b/packages/backend/src/server/api/endpoints/users/achievements.ts
index e4845d57bf..d6ad718dfa 100644
--- a/packages/backend/src/server/api/endpoints/users/achievements.ts
+++ b/packages/backend/src/server/api/endpoints/users/achievements.ts
@@ -10,6 +10,21 @@ import { DI } from '@/di-symbols.js';
 
 export const meta = {
 	requireCredential: true,
+
+	res: {
+		type: 'array',
+		items: {
+			type: 'object',
+			properties: {
+				name: {
+					type: 'string',
+				},
+				unlockedAt: {
+					type: 'number',
+				},
+			},
+		},
+	}
 } as const;
 
 export const paramDef = {
diff --git a/packages/backend/src/server/api/endpoints/users/lists/get-memberships.ts b/packages/backend/src/server/api/endpoints/users/lists/get-memberships.ts
index ae8b4e9b81..985141515e 100644
--- a/packages/backend/src/server/api/endpoints/users/lists/get-memberships.ts
+++ b/packages/backend/src/server/api/endpoints/users/lists/get-memberships.ts
@@ -25,6 +25,35 @@ export const meta = {
 			id: '7bc05c21-1d7a-41ae-88f1-66820f4dc686',
 		},
 	},
+
+	res: {
+		type: 'array',
+		items: {
+			type: 'object',
+			nullable: false,
+			properties: {
+				id: {
+					type: 'string',
+					format: 'misskey:id',
+				},
+				createdAt: {
+					type: 'string',
+					format: 'date-time',
+				},
+				userId: {
+					type: 'string',
+					format: 'misskey:id',
+				},
+				user: {
+					type: 'object',
+					ref: 'User',
+				},
+				withReplies: {
+					type: 'boolean',
+				},
+			},
+		},
+	},
 } as const;
 
 export const paramDef = {

From 433d46e57f127ae7b3dc3715e9363790425eeabf 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, 22 Dec 2023 09:06:13 +0900
Subject: [PATCH 03/25] =?UTF-8?q?fix(backend):=20=E3=83=86=E3=82=B9?=
 =?UTF-8?q?=E3=83=88=E3=81=8C=E6=AD=BB=E3=82=93=E3=81=A7=E3=81=84=E3=82=8B?=
 =?UTF-8?q?=E3=81=AE=E3=82=92=E4=BF=AE=E6=AD=A3=20(#12738)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

* fix test

* fix test

* fix test

* fix test

* fix test
---
 packages/backend/src/core/UserListService.ts | 13 ++++++++++---
 packages/backend/test/e2e/timelines.ts       | 15 +++++++--------
 packages/backend/test/unit/RoleService.ts    | 20 +++++++++++++++++---
 3 files changed, 34 insertions(+), 14 deletions(-)

diff --git a/packages/backend/src/core/UserListService.ts b/packages/backend/src/core/UserListService.ts
index 832b715d97..b6e4e1e884 100644
--- a/packages/backend/src/core/UserListService.ts
+++ b/packages/backend/src/core/UserListService.ts
@@ -3,8 +3,9 @@
  * SPDX-License-Identifier: AGPL-3.0-only
  */
 
-import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common';
+import { Inject, Injectable, OnApplicationShutdown, OnModuleInit } from '@nestjs/common';
 import * as Redis from 'ioredis';
+import { ModuleRef } from '@nestjs/core';
 import type { UserListMembershipsRepository } from '@/models/_.js';
 import type { MiUser } from '@/models/User.js';
 import type { MiUserList } from '@/models/UserList.js';
@@ -21,12 +22,15 @@ import { RedisKVCache } from '@/misc/cache.js';
 import { RoleService } from '@/core/RoleService.js';
 
 @Injectable()
-export class UserListService implements OnApplicationShutdown {
+export class UserListService implements OnApplicationShutdown, OnModuleInit {
 	public static TooManyUsersError = class extends Error {};
 
 	public membersCache: RedisKVCache<Set<string>>;
+	private roleService: RoleService;
 
 	constructor(
+		private moduleRef: ModuleRef,
+
 		@Inject(DI.redis)
 		private redisClient: Redis.Redis,
 
@@ -38,7 +42,6 @@ export class UserListService implements OnApplicationShutdown {
 
 		private userEntityService: UserEntityService,
 		private idService: IdService,
-		private roleService: RoleService,
 		private globalEventService: GlobalEventService,
 		private proxyAccountService: ProxyAccountService,
 		private queueService: QueueService,
@@ -54,6 +57,10 @@ export class UserListService implements OnApplicationShutdown {
 		this.redisForSub.on('message', this.onMessage);
 	}
 
+	async onModuleInit() {
+		this.roleService = this.moduleRef.get(RoleService.name);
+	}
+
 	@bindThis
 	private async onMessage(_: string, data: string): Promise<void> {
 		const obj = JSON.parse(data);
diff --git a/packages/backend/test/e2e/timelines.ts b/packages/backend/test/e2e/timelines.ts
index 73c446444b..cb9558b416 100644
--- a/packages/backend/test/e2e/timelines.ts
+++ b/packages/backend/test/e2e/timelines.ts
@@ -10,9 +10,8 @@ process.env.NODE_ENV = 'test';
 process.env.FORCE_FOLLOW_REMOTE_USER_FOR_TESTING = 'true';
 
 import * as assert from 'assert';
-import { signup, api, post, react, startServer, waitFire, sleep, uploadUrl, randomString } from '../utils.js';
+import { api, post, randomString, signup, sleep, startServer, uploadUrl } from '../utils.js';
 import type { INestApplicationContext } from '@nestjs/common';
-import type * as misskey from 'misskey-js';
 
 function genHost() {
 	return randomString() + '.example.com';
@@ -366,8 +365,8 @@ describe('Timelines', () => {
 			await api('/following/create', { userId: bob.id }, alice);
 			await sleep(1000);
 			const [bobFile, carolFile] = await Promise.all([
-				uploadUrl(bob, 'https://raw.githubusercontent.com/misskey-dev/assets/main/icon.png'),
-				uploadUrl(carol, 'https://raw.githubusercontent.com/misskey-dev/assets/main/icon.png'),
+				uploadUrl(bob, 'https://raw.githubusercontent.com/misskey-dev/assets/main/public/icon.png'),
+				uploadUrl(carol, 'https://raw.githubusercontent.com/misskey-dev/assets/main/public/icon.png'),
 			]);
 			const bobNote1 = await post(bob, { text: 'hi' });
 			const bobNote2 = await post(bob, { fileIds: [bobFile.id] });
@@ -666,7 +665,7 @@ describe('Timelines', () => {
 		test.concurrent('[withFiles: true] ファイル付きノートのみ含まれる', async () => {
 			const [alice, bob] = await Promise.all([signup(), signup()]);
 
-			const file = await uploadUrl(bob, 'https://raw.githubusercontent.com/misskey-dev/assets/main/icon.png');
+			const file = await uploadUrl(bob, 'https://raw.githubusercontent.com/misskey-dev/assets/main/public/icon.png');
 			const bobNote1 = await post(bob, { text: 'hi' });
 			const bobNote2 = await post(bob, { fileIds: [file.id] });
 
@@ -804,7 +803,7 @@ describe('Timelines', () => {
 		test.concurrent('[withFiles: true] ファイル付きノートのみ含まれる', async () => {
 			const [alice, bob] = await Promise.all([signup(), signup()]);
 
-			const file = await uploadUrl(bob, 'https://raw.githubusercontent.com/misskey-dev/assets/main/icon.png');
+			const file = await uploadUrl(bob, 'https://raw.githubusercontent.com/misskey-dev/assets/main/public/icon.png');
 			const bobNote1 = await post(bob, { text: 'hi' });
 			const bobNote2 = await post(bob, { fileIds: [file.id] });
 
@@ -999,7 +998,7 @@ describe('Timelines', () => {
 
 			const list = await api('/users/lists/create', { name: 'list' }, alice).then(res => res.body);
 			await api('/users/lists/push', { listId: list.id, userId: bob.id }, alice);
-			const file = await uploadUrl(bob, 'https://raw.githubusercontent.com/misskey-dev/assets/main/icon.png');
+			const file = await uploadUrl(bob, 'https://raw.githubusercontent.com/misskey-dev/assets/main/public/icon.png');
 			const bobNote1 = await post(bob, { text: 'hi' });
 			const bobNote2 = await post(bob, { fileIds: [file.id] });
 
@@ -1158,7 +1157,7 @@ describe('Timelines', () => {
 		test.concurrent('[withFiles: true] ファイル付きノートのみ含まれる', async () => {
 			const [alice, bob] = await Promise.all([signup(), signup()]);
 
-			const file = await uploadUrl(bob, 'https://raw.githubusercontent.com/misskey-dev/assets/main/icon.png');
+			const file = await uploadUrl(bob, 'https://raw.githubusercontent.com/misskey-dev/assets/main/public/icon.png');
 			const bobNote1 = await post(bob, { text: 'hi' });
 			const bobNote2 = await post(bob, { fileIds: [file.id] });
 
diff --git a/packages/backend/test/unit/RoleService.ts b/packages/backend/test/unit/RoleService.ts
index 99c6912116..9879eb8e3e 100644
--- a/packages/backend/test/unit/RoleService.ts
+++ b/packages/backend/test/unit/RoleService.ts
@@ -73,13 +73,21 @@ describe('RoleService', () => {
 				CacheService,
 				IdService,
 				GlobalEventService,
+				{
+					provide: NotificationService,
+					useFactory: () => ({
+						createNotification: jest.fn(),
+					}),
+				},
+				{
+					provide: NotificationService.name,
+					useExisting: NotificationService,
+				},
 			],
 		})
 			.useMocker((token) => {
 				if (token === MetaService) {
 					return { fetch: jest.fn() };
-				} else if (token === NotificationService) {
-					return { createNotification: jest.fn() };
 				}
 				if (typeof token === 'function') {
 					const mockMetadata = moduleMocker.getMetadata(token) as MockFunctionMetadata<any, any>;
@@ -98,6 +106,8 @@ describe('RoleService', () => {
 
 		metaService = app.get<MetaService>(MetaService) as jest.Mocked<MetaService>;
 		notificationService = app.get<NotificationService>(NotificationService) as jest.Mocked<NotificationService>;
+
+		await roleService.onModuleInit();
 	});
 
 	afterEach(async () => {
@@ -284,10 +294,12 @@ describe('RoleService', () => {
 			const user = await createUser();
 			const role = await createRole({
 				isPublic: true,
+				name: 'a',
 			});
 
 			await roleService.assign(user.id, role.id);
 
+			clock.uninstall();
 			await sleep(100);
 
 			const assignments = await roleAssignmentsRepository.find({
@@ -301,7 +313,7 @@ describe('RoleService', () => {
 			expect(notificationService.createNotification).toHaveBeenCalled();
 			expect(notificationService.createNotification.mock.lastCall![0]).toBe(user.id);
 			expect(notificationService.createNotification.mock.lastCall![1]).toBe('roleAssigned');
-			expect(notificationService.createNotification.mock.lastCall![2]).toBe({
+			expect(notificationService.createNotification.mock.lastCall![2]).toEqual({
 				roleId: role.id,
 			});
 		});
@@ -310,10 +322,12 @@ describe('RoleService', () => {
 			const user = await createUser();
 			const role = await createRole({
 				isPublic: false,
+				name: 'a',
 			});
 
 			await roleService.assign(user.id, role.id);
 
+			clock.uninstall();
 			await sleep(100);
 
 			const assignments = await roleAssignmentsRepository.find({

From 52b94dbc4ab60aa6efba927ef8df509a3bb0d046 Mon Sep 17 00:00:00 2001
From: anatawa12 <anatawa12@icloud.com>
Date: Fri, 22 Dec 2023 14:03:39 +0900
Subject: [PATCH 04/25] =?UTF-8?q?fix:=20=E5=BC=95=E7=94=A8RN=E3=81=8Cpure?=
 =?UTF-8?q?=20RN=E3=81=A8=E3=81=97=E3=81=A6=E9=80=A3=E5=90=88=E3=81=95?=
 =?UTF-8?q?=E3=82=8C=E3=80=81pure=20RN=E3=81=8C=E5=BC=95=E7=94=A8RN?=
 =?UTF-8?q?=E3=81=A8=E3=81=97=E3=81=A6=E9=80=A3=E5=90=88=E3=81=95=E3=82=8C?=
 =?UTF-8?q?=E3=82=8B=20(#12744)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

* fix: quote notes are rendered as pure renote

* fix: filesが指定されてて空配列のときにQuote扱いされる

* chore: isQuoteの仕様をmisc/is-quote.tsと揃える

* docs: is-quote.tsの方にNoteCreateService.isQuoteのことを書いて更新忘れを防ぐ
---
 packages/backend/src/core/NoteCreateService.ts | 9 +++++----
 packages/backend/src/misc/is-quote.ts          | 1 +
 2 files changed, 6 insertions(+), 4 deletions(-)

diff --git a/packages/backend/src/core/NoteCreateService.ts b/packages/backend/src/core/NoteCreateService.ts
index 9fe965b139..54493612b8 100644
--- a/packages/backend/src/core/NoteCreateService.ts
+++ b/packages/backend/src/core/NoteCreateService.ts
@@ -293,7 +293,7 @@ export class NoteCreateService implements OnApplicationShutdown {
 		}
 
 		// Check blocking
-		if (data.renote && this.isQuote(data)) {
+		if (this.isQuote(data)) {
 			if (data.renote.userHost === null) {
 				if (data.renote.userId !== user.id) {
 					const blocked = await this.userBlockingService.checkBlocked(data.renote.userId, user.id);
@@ -730,8 +730,9 @@ export class NoteCreateService implements OnApplicationShutdown {
 	}
 
 	@bindThis
-	private isQuote(note: Option): boolean {
-		return !!note.text || !!note.cw || !!note.files || !!note.poll;
+	private isQuote(note: Option): note is Option & { renote: MiNote } {
+		// sync with misc/is-quote.ts
+		return !!note.renote && (!!note.text || !!note.cw || (!!note.files && !!note.files.length) || !!note.poll);
 	}
 
 	@bindThis
@@ -799,7 +800,7 @@ export class NoteCreateService implements OnApplicationShutdown {
 	private async renderNoteOrRenoteActivity(data: Option, note: MiNote) {
 		if (data.localOnly) return null;
 
-		const content = data.renote && this.isQuote(data)
+		const content = data.renote && !this.isQuote(data)
 			? this.apRendererService.renderAnnounce(data.renote.uri ? data.renote.uri : `${this.config.url}/notes/${data.renote.id}`, note)
 			: this.apRendererService.renderCreate(await this.apRendererService.renderNote(note, false), note);
 
diff --git a/packages/backend/src/misc/is-quote.ts b/packages/backend/src/misc/is-quote.ts
index 059f6a4b5f..db72d1d57a 100644
--- a/packages/backend/src/misc/is-quote.ts
+++ b/packages/backend/src/misc/is-quote.ts
@@ -7,5 +7,6 @@ import type { MiNote } from '@/models/Note.js';
 
 // eslint-disable-next-line import/no-default-export
 export default function(note: MiNote): boolean {
+	// sync with NoteCreateService.isQuote
 	return note.renoteId != null && (note.text != null || note.hasPoll || (note.fileIds != null && note.fileIds.length > 0));
 }

From d68214bd463d96aadd88e412f8106301811423bf Mon Sep 17 00:00:00 2001
From: zyoshoka <107108195+zyoshoka@users.noreply.github.com>
Date: Fri, 22 Dec 2023 15:38:27 +0900
Subject: [PATCH 05/25] =?UTF-8?q?fix(i18n):=20=E3=83=AD=E3=83=BC=E3=83=AB?=
 =?UTF-8?q?=E3=81=8C=E4=BB=98=E4=B8=8E=E3=81=95=E3=82=8C=E3=81=9F=E9=9A=9B?=
 =?UTF-8?q?=E3=81=AE=E9=80=9A=E7=9F=A5=E3=81=AE=E3=83=AD=E3=83=BC=E3=82=AB?=
 =?UTF-8?q?=E3=83=A9=E3=82=A4=E3=82=BC=E3=83=BC=E3=82=B7=E3=83=A7=E3=83=B3?=
 =?UTF-8?q?=E3=81=8C=E4=B8=80=E9=83=A8=E6=AC=A0=E3=81=91=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(#12745)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 locales/index.d.ts | 1 +
 locales/ja-JP.yml  | 1 +
 2 files changed, 2 insertions(+)

diff --git a/locales/index.d.ts b/locales/index.d.ts
index f22b7f1c4a..fd96fd7625 100644
--- a/locales/index.d.ts
+++ b/locales/index.d.ts
@@ -2347,6 +2347,7 @@ export interface Locale {
             "pollEnded": string;
             "receiveFollowRequest": string;
             "followRequestAccepted": string;
+            "roleAssigned": string;
             "achievementEarned": string;
             "app": string;
         };
diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml
index 2185183c98..2c29bd20da 100644
--- a/locales/ja-JP.yml
+++ b/locales/ja-JP.yml
@@ -2250,6 +2250,7 @@ _notification:
     pollEnded: "アンケートが終了"
     receiveFollowRequest: "フォロー申請を受け取った"
     followRequestAccepted: "フォローが受理された"
+    roleAssigned: "ロールが付与された"
     achievementEarned: "実績の獲得"
     app: "連携アプリからの通知"
 

From 6d4aa316ac886870ad98be09ccd4c5d03cf7a9c4 Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Fri, 22 Dec 2023 18:05:41 +0900
Subject: [PATCH 06/25] New Crowdin updates (#12732)

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

* New translations ja-jp.yml (Korean)

* New translations ja-jp.yml (French)

* New translations ja-jp.yml (Catalan)

* New translations ja-jp.yml (Catalan)

* New translations ja-jp.yml (English)
---
 locales/ca-ES.yml | 158 ++++++++++++++++++++++++++++++++++++++++++++++
 locales/en-US.yml |   7 +-
 locales/fr-FR.yml |   1 +
 locales/ko-KR.yml |   1 +
 locales/zh-TW.yml |   1 +
 5 files changed, 166 insertions(+), 2 deletions(-)

diff --git a/locales/ca-ES.yml b/locales/ca-ES.yml
index b4fa799ada..727e473cf3 100644
--- a/locales/ca-ES.yml
+++ b/locales/ca-ES.yml
@@ -121,6 +121,12 @@ sensitive: "NSFW"
 add: "Afegir"
 reaction: "Reaccions"
 reactions: "Reaccions"
+emojiPicker: "Selecció d'emojis"
+pinnedEmojisForReactionSettingDescription: "Selecciona l'emoji amb el qual reaccionar"
+pinnedEmojisSettingDescription: "Selecciona l'emoji amb el qual reaccionar"
+emojiPickerDisplay: "Visualitza el selector d'emojis"
+overwriteFromPinnedEmojisForReaction: "Reemplaça els emojis de la reacció"
+overwriteFromPinnedEmojis: "Sobreescriu des dels emojis fixats"
 reactionSettingDescription2: "Arrossega per reordenar, fes clic per suprimir, prem \"+\" per afegir."
 rememberNoteVisibility: "Recorda la configuració de visibilitat de les notes"
 attachCancel: "Eliminar el fitxer adjunt"
@@ -213,6 +219,9 @@ clearQueueConfirmText: "Les notes no lliurades que quedin a la cua no es federar
 clearCachedFiles: "Esborra la memòria cau"
 clearCachedFilesConfirm: "Segur que voleu eliminar tots els fitxers de la memòria cau?"
 blockedInstances: "Instàncies bloquejades"
+blockedInstancesDescription: "Llista els enllaços d'amfitrió de les instàncies que vols bloquejar separades per un salt de pàgina. Les instàncies llistades no podran comunicar-se amb aquesta instància."
+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."
 muteAndBlock: "Silencia i bloca"
 mutedUsers: "Usuaris silenciats"
 blockedUsers: "Usuaris bloquejats"
@@ -227,9 +236,12 @@ preview: "Vista prèvia"
 default: "Per defecte"
 defaultValueIs: "Per defecte: {value}"
 noCustomEmojis: "Cap emoji personalitzat"
+noJobs: "No hi ha feines"
 federating: "Federant"
 blocked: "Bloquejat"
 suspended: "Suspés"
+all: "tot"
+subscribing: "Subscrit a"
 publishing: "S'està publicant"
 notResponding: "Sense resposta"
 instanceFollowing: "Seguits del servidor"
@@ -254,11 +266,31 @@ removed: "Eliminat"
 removeAreYouSure: "Segur que voleu retirar «{x}»?"
 deleteAreYouSure: "Segur que voleu retirar «{x}»?"
 resetAreYouSure: "Segur que voleu restablir-ho?"
+areYouSure: "Està segur?"
 saved: "S'ha desat"
 messaging: "Xat"
 upload: "Puja"
+keepOriginalUploading: "Guarda la imatge original"
+keepOriginalUploadingDescription: "Guarda la imatge pujada com hi és. Si està apagat, una versió per a la visualització a la xarxa serà generada quan sigui pujada."
+fromDrive: "Des de la unitat"
+fromUrl: "Des d'un enllaç"
+uploadFromUrl: "Carrega des d'un enllaç"
+uploadFromUrlDescription: "Enllaç del fitxer que vols carregar"
+uploadFromUrlRequested: "Càrrega sol·licitada"
+uploadFromUrlMayTakeTime: "La càrrega des de l'enllaç pot prendre un temps"
+explore: "Explora"
+messageRead: "Vist"
+noMoreHistory: "No hi resta més per veure"
+startMessaging: "Començar a xatejar"
+nUsersRead: "Vist per {n}"
+agreeTo: "Accepto que {0}"
+agree: "Hi estic d'acord"
+agreeBelow: "Hi estic d'acord amb el següent"
+basicNotesBeforeCreateAccount: "Notes importants"
+termsOfService: "Condicions d'ús"
 start: "Comença"
 home: "Inici"
+remoteUserCaution: "Ja que aquest usuari resideix a una instància remota, la informació mostrada es podria trobar incompleta."
 activity: "Activitat"
 images: "Imatges"
 image: "Imatges"
@@ -274,16 +306,34 @@ dark: "Fosc"
 lightThemes: "Temes clars"
 darkThemes: "Temes foscos"
 syncDeviceDarkMode: "Sincronitza el mode fosc amb la configuració del dispositiu"
+drive: "Unitat"
+fileName: "Nom del Fitxer"
+selectFile: "Selecciona fitxers"
+selectFiles: "Selecciona fitxers"
+selectFolder: "Selecció de carpeta"
+selectFolders: "Selecció de carpeta"
 renameFile: "Canvia el nom del fitxer"
 folderName: "Nom de la carpeta"
 createFolder: "Crea una carpeta"
 renameFolder: "Canvia el nom de la carpeta"
 deleteFolder: "Elimina la carpeta"
+folder: "Carpeta "
 addFile: "Afegeix un fitxer"
+emptyDrive: "La teva unitat és buida"
 emptyFolder: "La carpeta està buida"
 unableToDelete: "No es pot eliminar"
+inputNewFileName: "Introduïu el nom de fitxer nou"
+inputNewDescription: "Inserta una nova llegenda"
+inputNewFolderName: "Introduïu el nom de la carpeta nova"
+circularReferenceFolder: "La carpeta destinatària és una subcarpeta de la carpeta a la qual la desitges moure"
+hasChildFilesOrFolders: "No és possible esborrar aquesta carpeta ja que no és buida"
 copyUrl: "Copia l'URL"
 rename: "Canvia el nom"
+avatar: "Icona"
+banner: "Bàner"
+displayOfSensitiveMedia: "Visualització de contingut sensible"
+whenServerDisconnected: "Quan es perdi la connexió al servidor"
+disconnectedFromServer: "Desconnectat pel servidor"
 reload: "Actualitza"
 doNothing: "Ignora"
 accept: "Accepta"
@@ -353,33 +403,132 @@ notFound: "No s'ha trobat"
 markAsReadAllUnreadNotes: "Marca-ho tot com a llegit"
 help: "Ajuda"
 invites: "Convida"
+title: "Títol"
+text: "Text"
+enable: "Habilita"
 next: "Següent"
+retype: "Torneu a introduir-la"
 noteOf: "Publicació de: {user}"
+quoteAttached: "Frase adjunta"
+quoteQuestion: "Vols annexar-la com a cita?"
+noMessagesYet: "Encara no hi ha missatges"
+newMessageExists: "Has rebut un nou missatge"
+onlyOneFileCanBeAttached: "Només pots adjuntar un fitxer a un missatge"
+signinRequired: "Si us plau, Registra't o inicia la sessió abans de continuar"
 invitations: "Convida"
+invitationCode: "Codi d'invitació"
+checking: "Comprovació en curs..."
+available: "Disponible"
+unavailable: "No és disponible"
+usernameInvalidFormat: "Pots fer servir lletres (majúscules i minúscules), números i barres baixes (\"_\")"
+tooShort: "Massa curt"
+tooLong: "Massa llarg"
+weakPassword: "Contrasenya insegura"
+normalPassword: "Bona contrasenya"
+strongPassword: "Contrasenya segura"
+passwordMatched: "Correcte!"
+passwordNotMatched: "No coincideix"
+signinWith: "Inicia sessió amb amb {x}"
+signinFailed: "Autenticació sense èxit. Intenta-ho un altre cop utilitzant la contrasenya i el nom correctes."
+or: "O"
+language: "Idioma"
+uiLanguage: "Idioma de l'interfície"
+aboutX: "Respecte a {x}"
+emojiStyle: "Estil d'emoji"
+native: "Nadiu"
+disableDrawer: "No mostrar els menús en calaixos"
+showNoteActionsOnlyHover: "Només mostra accions de la nota en passar amb el cursor"
+noHistory: "No hi ha un registre previ"
+signinHistory: "Historial d'autenticacions"
+enableAdvancedMfm: "Habilitar l'MFM avançat"
+enableAnimatedMfm: "Habilitar l'MFM amb moviment"
+doing: "Processant..."
+category: "Categoria"
 tags: "Etiquetes"
 docSource: "Font del document"
 createAccount: "Crea un compte"
 existingAccount: "Compte existent"
 regenerate: "Regenera"
 fontSize: "Mida del text"
+mediaListWithOneImageAppearance: "Altura de la llista de fitxers amb una única imatge"
+limitTo: "Limita a {x}"
 noFollowRequests: "No tens sol·licituds de seguiment"
+openImageInNewTab: "Obre imatges a una nova pestanya"
 dashboard: "Panell de control"
 local: "Local"
 remote: "Remot"
 total: "Total"
+weekOverWeekChanges: "Canvis l'última setmana"
+dayOverDayChanges: "Canvis ahir"
 appearance: "Aparença"
 clientSettings: "Configuració del client"
 accountSettings: "Configuració del compte"
+promotion: "Promocionat"
+promote: "Promoure"
+numberOfDays: "Nombre de dies"
 hideThisNote: "Amaga la publicació"
 showFeaturedNotesInTimeline: "Mostra publicacions destacades en la línia de temps"
+objectStorage: "Emmagatzematge d'objectes\n"
+useObjectStorage: "Utilitzar l'emmagatzematge d'objectes"
+objectStorageBaseUrl: "Base d'enllaç"
+objectStorageBaseUrlDesc: "Prefix d'enllaç utilitzat per a fer referencia als fitxers. Especifica l'enllaç del teu CDN o Proxy si n'estàs utilitzant qualsevol, en cas contrari, especifica l'enllaç al que es pot accedir públicament segons la guia de servei que vosté utilitza.\nPer l'ús d'S3 utilitza 'https://<bucket>.s3.amazonaws.com' I per a GCS o serveis equivalents utilitza 'https://storage.googleapis.com/<bucket>'."
 newNoteRecived: "Hi ha publicacions noves"
 installedDate: "Data d'instal·lació"
 state: "Estat"
 sort: "Ordena"
 ascendingOrder: "Ascendent"
 descendingOrder: "Descendent"
+removeAllFollowing: "Deixar de seguir tots els usuaris seguits"
+removeAllFollowingDescription: "El fet d'executar això, et farà deixar de seguir a tots els usuaris de {host}. Si us plau, executa això si l'amfitrió, per exemple, ja no existeix."
+userSuspended: "Aquest usuari ha sigut suspès"
+userSilenced: "Aquest usuari està sent silenciat"
+yourAccountSuspendedTitle: "Aquest compte és suspès"
+yourAccountSuspendedDescription: "Aquest compte ha sigut suspès a causa de la violació de les condicions d'ús o similars. Contacta l'administrador si en vol saber més. Si us plau, no en faci un altre compte."
+tokenRevoked: "Codi de seguretat no vàlid"
+tokenRevokedDescription: "La petició més recent ha estat denegada perquè contenia un codi de seguretat no vàlid. Actualitza la pàgina i torna-ho a provar."
+accountDeleted: "Compte eliminat amb èxit"
+accountDeletedDescription: "Aquest compte ha sigut eliminat"
+menu: "Menú"
+divider: "Divisor"
+addItem: "Afegir element"
+rearrange: "Torna a ordenar"
+relays: "Relés"
+addRelay: "Afegeix relés"
+inboxUrl: "Enllaç de la safata d'entrada"
+addedRelays: "Relés afegits"
+serviceworkerInfo: "És obligatòria l'activació per a obtenir notificacions push"
 deletedNote: "Publicacions eliminades"
 invisibleNote: "Publicacions amagades"
+enableInfiniteScroll: "Carrega més automàticament\n"
+visibility: "Visibilitat"
+poll: "Enquesta"
+useCw: "Amaga el contingut"
+enablePlayer: "Obre el reproductor de vídeo"
+disablePlayer: "Tanca el reproductor de vídeo"
+expandTweet: "Expandir post"
+themeEditor: "Editor de temes"
+description: "Descripció"
+describeFile: "Afegir subtitulació"
+enterFileDescription: "Afegeix un títol"
+author: "Autor"
+leaveConfirm: "Hi ha canvis sense guardar. Els vols descartar?"
+manage: "Administració"
+plugins: "Extensions"
+preferencesBackups: "Configuracions de les Còpies de seguretat"
+deck: "Escriptori"
+undeck: "Tanca l'escriptori"
+useBlurEffectForModal: "Utilitzar l'efecte de difuminació a modals"
+useFullReactionPicker: "Utilitza el cercador de reaccions d'escala sencera"
+width: "Amplada"
+height: "Alçària"
+large: "Gran"
+medium: "Mitjà"
+small: "Petit"
+generateAccessToken: "Genera codi d'accés"
+permission: "Permisos"
+enableAll: "Habilita tot"
+disableAll: "Deshabilita tot"
+tokenRequested: "Donar accés al compte"
 smtpHost: "Amfitrió"
 smtpUser: "Nom d'usuari"
 smtpPass: "Contrasenya"
@@ -389,12 +538,17 @@ clearCache: "Esborra la memòria cau"
 showingPastTimeline: "Estàs veient una línia de temps antiga"
 info: "Informació"
 user: "Usuaris"
+administration: "Administració"
+middle: "Mitjà"
 global: "Global"
 searchByGoogle: "Cercar"
 file: "Fitxers"
+icon: "Icona"
 replies: "Respondre"
 renotes: "Impulsa"
 _role:
+  _priority:
+    middle: "Mitjà"
   _options:
     antennaMax: "Nombre màxim d'antenes"
 _email:
@@ -403,9 +557,11 @@ _email:
 _instanceMute:
   instanceMuteDescription: "Silencia tots els impulsos dels servidors seleccionats, també els usuaris que responen a altres d'un servidor silenciat."
 _theme:
+  description: "Descripció"
   keys:
     mention: "Menció"
     renote: "Renotar"
+    divider: "Divisor"
 _sfx:
   note: "Notes"
   notification: "Notificacions"
@@ -447,6 +603,8 @@ _timelines:
   local: "Local"
   social: "Social"
   global: "Global"
+_play:
+  summary: "Descripció"
 _pages:
   contents: "Contingut"
   blocks:
diff --git a/locales/en-US.yml b/locales/en-US.yml
index 65fe07b6d0..0701bc3710 100644
--- a/locales/en-US.yml
+++ b/locales/en-US.yml
@@ -543,7 +543,7 @@ showInPage: "Show in page"
 popout: "Pop-out"
 volume: "Volume"
 masterVolume: "Master volume"
-notUseSound: "No sounds output."
+notUseSound: "Disable sound"
 useSoundOnlyWhenActive: "Output sounds only if Misskey is active."
 details: "Details"
 chooseEmoji: "Select an emoji"
@@ -1167,6 +1167,7 @@ cwNotationRequired: "If \"Hide content\" is enabled, a description must be provi
 doReaction: "Add reaction"
 code: "Code"
 reloadRequiredToApplySettings: "Reloading is required to apply the settings."
+decorate: "Decorate"
 _announcement:
   forExistingUsers: "Existing users only"
   forExistingUsersDescription: "This announcement will only be shown to users existing at the point of publishment if enabled. If disabled, those newly signing up after it has been posted will also see it."
@@ -1256,7 +1257,7 @@ _initialTutorial:
     sensitiveSucceeded: "When attaching files, please set sensitivities in accordance with the server guidelines."
     doItToContinue: "Mark the attachment file as sensitive to proceed."
   _done:
-    title: "The tutorial is complete! 🎉"
+    title: "You've completed the tutorial! 🎉"
     description: "The functions introduced here are just a small part. For a more detailed understanding of using Misskey, please refer to {link}."
 _timelineDescription:
   home: "In the Home timeline, you can see notes from accounts you follow."
@@ -2154,6 +2155,7 @@ _notification:
   pollEnded: "Poll results have become available"
   newNote: "New note"
   unreadAntennaNote: "Antenna {name}"
+  roleAssigned: "Role given"
   emptyPushNotificationMessage: "Push notifications have been updated"
   achievementEarned: "Achievement unlocked"
   testNotification: "Test notification"
@@ -2175,6 +2177,7 @@ _notification:
     pollEnded: "Polls ending"
     receiveFollowRequest: "Received follow requests"
     followRequestAccepted: "Accepted follow requests"
+    roleAssigned: "Role given"
     achievementEarned: "Achievement unlocked"
     app: "Notifications from linked apps"
   _actions:
diff --git a/locales/fr-FR.yml b/locales/fr-FR.yml
index 8acbc7d7a6..e12b508617 100644
--- a/locales/fr-FR.yml
+++ b/locales/fr-FR.yml
@@ -1899,6 +1899,7 @@ _notification:
   yourFollowRequestAccepted: "Votre demande d’abonnement a été accepté"
   pollEnded: "Les résultats du sondage sont disponibles"
   unreadAntennaNote: "Antenne {name}"
+  roleAssigned: "Rôle attribué"
   emptyPushNotificationMessage: "Les notifications push ont été mises à jour"
   achievementEarned: "Accomplissement"
   testNotification: "Tester la notification"
diff --git a/locales/ko-KR.yml b/locales/ko-KR.yml
index c8d69255f9..d8efa7f04e 100644
--- a/locales/ko-KR.yml
+++ b/locales/ko-KR.yml
@@ -2171,6 +2171,7 @@ _notification:
   pollEnded: "투표 결과가 발표되었습니다"
   newNote: "새 게시물"
   unreadAntennaNote: "안테나 {name}"
+  roleAssigned: "역할이 부여 되었습니다."
   emptyPushNotificationMessage: "푸시 알림이 갱신되었습니다"
   achievementEarned: "도전 과제를 달성했습니다"
   testNotification: "알림 테스트"
diff --git a/locales/zh-TW.yml b/locales/zh-TW.yml
index 419c063e27..d05691d42e 100644
--- a/locales/zh-TW.yml
+++ b/locales/zh-TW.yml
@@ -2171,6 +2171,7 @@ _notification:
   pollEnded: "問卷調查已產生結果"
   newNote: "新的貼文"
   unreadAntennaNote: "天線 {name}"
+  roleAssigned: "已授予角色"
   emptyPushNotificationMessage: "推送通知已更新"
   achievementEarned: "獲得成就"
   testNotification: "通知測試"

From 179cb1d8139ac9ffdc3b5e527e0008f9b7422067 Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Fri, 22 Dec 2023 20:14:20 +0900
Subject: [PATCH 07/25] fix type

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

diff --git a/packages/frontend/src/components/global/MkPageHeader.vue b/packages/frontend/src/components/global/MkPageHeader.vue
index 301e691fa0..8624aebdcf 100644
--- a/packages/frontend/src/components/global/MkPageHeader.vue
+++ b/packages/frontend/src/components/global/MkPageHeader.vue
@@ -53,7 +53,7 @@ import { PageHeaderItem } from '@/types/page-header.js';
 const props = withDefaults(defineProps<{
 	tabs?: Tab[];
 	tab?: string;
-	actions?: PageHeaderItem[];
+	actions?: PageHeaderItem[] | null;
 	thin?: boolean;
 	displayMyAvatar?: boolean;
 }>(), {

From 3d4af183274353fb7b1ea0ab0c9f8819d6e329be 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, 22 Dec 2023 20:16:31 +0900
Subject: [PATCH 08/25] =?UTF-8?q?[Hub=20Next]=20Misskey=20Hub=E3=81=AE?=
 =?UTF-8?q?=E3=83=AA=E3=83=B3=E3=82=AF=E3=82=92=E5=A4=89=E6=9B=B4=20(#1269?=
 =?UTF-8?q?9)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

* misskey hub のリンクを変更

* misskey-hub.net -> assets.misskey-hub.net
Related to misskey-dev/misskey-hub-next#57
---
 CHANGELOG.md                                  |  4 +-
 README.md                                     |  6 +--
 .../frontend/src/components/MkDonation.vue    |  2 +-
 .../src/components/MkSignupDialog.rules.vue   |  2 +-
 .../components/MkTutorialDialog.Timeline.vue  |  2 +-
 .../src/components/MkTutorialDialog.vue       |  2 +-
 .../frontend/src/components/MkUpdated.vue     |  2 +-
 .../src/components/MkVisitorDashboard.vue     |  4 +-
 packages/frontend/src/pages/about-misskey.vue | 48 +++++++++----------
 packages/frontend/src/pages/share.vue         |  2 +-
 packages/frontend/src/ui/_common_/common.ts   |  2 +-
 11 files changed, 38 insertions(+), 38 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index a7135d8745..95f435b7d7 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -152,7 +152,7 @@
 ### General
 - Feat: アイコンデコレーション機能
 	- サーバーで用意された画像をアイコンに重ねることができます
-	- 画像のテンプレートはこちらです: https://misskey-hub.net/avatar-decoration-template.png
+	- 画像のテンプレートはこちらです: https://misskey-hub.net/brand-assets/
 		- 最大でも黄色いエリア内にデコレーションを収めることを推奨します。
 		- 画像は512x512pxを推奨します。
 - Feat: チャンネル設定にリノート/引用リノートの可否を設定できる項目を追加
@@ -169,7 +169,7 @@
 ### Client
 - Feat: プラグイン・テーマを外部サイトから直接インストールできるようになりました
 	- 外部サイトでの実装が必要です。詳細は Misskey Hub をご覧ください
-	  https://misskey-hub.net/docs/advanced/publish-on-your-website.html
+	  https://misskey-hub.net/docs/for-developers/publish-on-your-website/
 - Feat: 通知をグルーピングして表示するオプション(オプトアウト)
 - Feat: Misskeyの基本的なチュートリアルを実装
 - Feat: スワイプしてタイムラインを再読込できるように
diff --git a/README.md b/README.md
index ab4388c2eb..6fa804f1fa 100644
--- a/README.md
+++ b/README.md
@@ -7,10 +7,10 @@
 
 ---
 
-<a href="https://misskey-hub.net/instances.html">
+<a href="https://misskey-hub.net/servers/">
 		<img src="https://custom-icon-badges.herokuapp.com/badge/find_an-instance-acea31?logoColor=acea31&style=for-the-badge&logo=misskey&labelColor=363B40" alt="find an instance"/></a>
 
-<a href="https://misskey-hub.net/docs/install.html">
+<a href="https://misskey-hub.net/docs/for-admin/install/guides/">
 		<img src="https://custom-icon-badges.herokuapp.com/badge/create_an-instance-FBD53C?logoColor=FBD53C&style=for-the-badge&logo=server&labelColor=363B40" alt="create an instance"/></a>
 
 <a href="./CONTRIBUTING.md">
@@ -51,7 +51,7 @@ With Misskey's built in drive, you get cloud storage right in your social media,
 
 ## Documentation
 
-Misskey Documentation can be found at [Misskey Hub](https://misskey-hub.net/), some of the links and graphics above also lead to specific portions of it.
+Misskey Documentation can be found at [Misskey Hub](https://misskey-hub.net/docs/), some of the links and graphics above also lead to specific portions of it.
 
 ## Sponsors
 
diff --git a/packages/frontend/src/components/MkDonation.vue b/packages/frontend/src/components/MkDonation.vue
index a2a0b6023b..3a1bab5f98 100644
--- a/packages/frontend/src/components/MkDonation.vue
+++ b/packages/frontend/src/components/MkDonation.vue
@@ -23,7 +23,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 				</template>
 			</I18n>
 			<div style="margin-top: 0.2em;">
-				<MkLink target="_blank" url="https://misskey-hub.net/docs/donate.html">{{ i18n.ts.learnMore }}</MkLink>
+				<MkLink target="_blank" url="https://misskey-hub.net/docs/for-users/resources/donate/">{{ i18n.ts.learnMore }}</MkLink>
 			</div>
 		</div>
 		<div class="_buttons">
diff --git a/packages/frontend/src/components/MkSignupDialog.rules.vue b/packages/frontend/src/components/MkSignupDialog.rules.vue
index 8f9c1c93f8..8cf7ce92ad 100644
--- a/packages/frontend/src/components/MkSignupDialog.rules.vue
+++ b/packages/frontend/src/components/MkSignupDialog.rules.vue
@@ -45,7 +45,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 				<template #label>{{ i18n.ts.basicNotesBeforeCreateAccount }}</template>
 				<template #suffix><i v-if="agreeNote" class="ti ti-check" style="color: var(--success)"></i></template>
 
-				<a href="https://misskey-hub.net/docs/notes.html" class="_link" target="_blank">{{ i18n.ts.basicNotesBeforeCreateAccount }} <i class="ti ti-external-link"></i></a>
+				<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>
 
 				<MkSwitch :modelValue="agreeNote" style="margin-top: 16px;" data-cy-signup-rules-notes-agree @update:modelValue="updateAgreeNote">{{ i18n.ts.agree }}</MkSwitch>
 			</MkFolder>
diff --git a/packages/frontend/src/components/MkTutorialDialog.Timeline.vue b/packages/frontend/src/components/MkTutorialDialog.Timeline.vue
index 75b917f33c..93181cf2b1 100644
--- a/packages/frontend/src/components/MkTutorialDialog.Timeline.vue
+++ b/packages/frontend/src/components/MkTutorialDialog.Timeline.vue
@@ -19,7 +19,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 	<div :class="$style.divider"></div>
 	<I18n :src="i18n.ts._initialTutorial._timeline.description3" tag="div" style="padding: 0 16px;">
 		<template #link>
-			<a href="https://misskey-hub.net/docs/features/timeline.html" target="_blank" class="_link">{{ i18n.ts.help }}</a>
+			<a href="https://misskey-hub.net/docs/for-users/features/timeline/" target="_blank" class="_link">{{ i18n.ts.help }}</a>
 		</template>
 	</I18n>
 
diff --git a/packages/frontend/src/components/MkTutorialDialog.vue b/packages/frontend/src/components/MkTutorialDialog.vue
index e28838425f..963e78a1ff 100644
--- a/packages/frontend/src/components/MkTutorialDialog.vue
+++ b/packages/frontend/src/components/MkTutorialDialog.vue
@@ -130,7 +130,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 							<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>
-									<a href="https://misskey-hub.net/help.html" target="_blank" class="_link">{{ i18n.ts.help }}</a>
+									<a href="https://misskey-hub.net/docs/for-users/" target="_blank" class="_link">{{ i18n.ts.help }}</a>
 								</template>
 							</I18n>
 							<div>{{ i18n.t('_initialAccountSetting.haveFun', { name: instance.name ?? host }) }}</div>
diff --git a/packages/frontend/src/components/MkUpdated.vue b/packages/frontend/src/components/MkUpdated.vue
index 699d7af33e..391733931a 100644
--- a/packages/frontend/src/components/MkUpdated.vue
+++ b/packages/frontend/src/components/MkUpdated.vue
@@ -27,7 +27,7 @@ const modal = shallowRef<InstanceType<typeof MkModal>>();
 
 const whatIsNew = () => {
 	modal.value.close();
-	window.open(`https://misskey-hub.net/docs/releases.html#_${version.replace(/\./g, '-')}`, '_blank');
+	window.open(`https://misskey-hub.net/docs/releases/#_${version.replace(/\./g, '')}`, '_blank');
 };
 
 onMounted(() => {
diff --git a/packages/frontend/src/components/MkVisitorDashboard.vue b/packages/frontend/src/components/MkVisitorDashboard.vue
index 60068df842..9ed08ee372 100644
--- a/packages/frontend/src/components/MkVisitorDashboard.vue
+++ b/packages/frontend/src/components/MkVisitorDashboard.vue
@@ -123,13 +123,13 @@ function showMenu(ev) {
 		text: i18n.ts.help,
 		icon: 'ti ti-help-circle',
 		action: () => {
-			window.open('https://misskey-hub.net/help.md', '_blank', 'noopener');
+			window.open('https://misskey-hub.net/docs/for-users/', '_blank', 'noopener');
 		},
 	}], ev.currentTarget ?? ev.target);
 }
 
 function exploreOtherServers() {
-	window.open('https://join.misskey.page/instances', '_blank', 'noopener');
+	window.open('https://misskey-hub.net/servers/', '_blank', 'noopener');
 }
 </script>
 
diff --git a/packages/frontend/src/pages/about-misskey.vue b/packages/frontend/src/pages/about-misskey.vue
index c245b9b6cb..20c65f4541 100644
--- a/packages/frontend/src/pages/about-misskey.vue
+++ b/packages/frontend/src/pages/about-misskey.vue
@@ -22,7 +22,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 					<button v-if="thereIsTreasure" class="_button treasure" @click="getTreasure"><img src="/fluent-emoji/1f3c6.png" class="treasureImg"></button>
 				</div>
 				<div style="text-align: center;">
-					{{ i18n.ts._aboutMisskey.about }}<br><a href="https://misskey-hub.net/docs/misskey.html" target="_blank" class="_link">{{ i18n.ts.learnMore }}</a>
+					{{ i18n.ts._aboutMisskey.about }}<br><a href="https://misskey-hub.net/docs/about-misskey/" target="_blank" class="_link">{{ i18n.ts.learnMore }}</a>
 				</div>
 				<div v-if="$i != null" style="text-align: center;">
 					<MkButton primary rounded inline @click="iLoveMisskey">I <Mfm text="$[jelly ❤]"/> #Misskey</MkButton>
@@ -139,73 +139,73 @@ import { $i } from '@/account.js';
 
 const patronsWithIcon = [{
 	name: 'カイヤン',
-	icon: 'https://misskey-hub.net/patrons/a2820716883e408cb87773e377ce7c8d.jpg',
+	icon: 'https://assets.misskey-hub.net/patrons/a2820716883e408cb87773e377ce7c8d.jpg',
 }, {
 	name: 'だれかさん',
-	icon: 'https://misskey-hub.net/patrons/f7409b5e5a88477a9b9d740c408de125.jpg',
+	icon: 'https://assets.misskey-hub.net/patrons/f7409b5e5a88477a9b9d740c408de125.jpg',
 }, {
 	name: 'narazaka',
-	icon: 'https://misskey-hub.net/patrons/e3affff31ffb4877b1196c7360abc3e5.jpg',
+	icon: 'https://assets.misskey-hub.net/patrons/e3affff31ffb4877b1196c7360abc3e5.jpg',
 }, {
 	name: 'ひとぅ',
-	icon: 'https://misskey-hub.net/patrons/8cc0d0a0a6d84c88bca1aedabf6ed5ab.jpg',
+	icon: 'https://assets.misskey-hub.net/patrons/8cc0d0a0a6d84c88bca1aedabf6ed5ab.jpg',
 }, {
 	name: 'ぱーこ',
-	icon: 'https://misskey-hub.net/patrons/79c6602ffade489e8df2fcf2c2bc5d9d.jpg',
+	icon: 'https://assets.misskey-hub.net/patrons/79c6602ffade489e8df2fcf2c2bc5d9d.jpg',
 }, {
 	name: 'わっほー☆',
-	icon: 'https://misskey-hub.net/patrons/d31d5d13924443a082f3da7966318a0a.jpg',
+	icon: 'https://assets.misskey-hub.net/patrons/d31d5d13924443a082f3da7966318a0a.jpg',
 }, {
 	name: 'mollinaca',
-	icon: 'https://misskey-hub.net/patrons/ceb36b8f66e549bdadb3b90d5da62314.jpg',
+	icon: 'https://assets.misskey-hub.net/patrons/ceb36b8f66e549bdadb3b90d5da62314.jpg',
 }, {
 	name: '坂本龍',
-	icon: 'https://misskey-hub.net/patrons/a631cf8b490145cf8dbbe4e7508cfbc2.jpg',
+	icon: 'https://assets.misskey-hub.net/patrons/a631cf8b490145cf8dbbe4e7508cfbc2.jpg',
 }, {
 	name: 'takke',
-	icon: 'https://misskey-hub.net/patrons/6c3327e626c046f2914fbcd9f7557935.jpg',
+	icon: 'https://assets.misskey-hub.net/patrons/6c3327e626c046f2914fbcd9f7557935.jpg',
 }, {
 	name: 'ぺんぎん',
-	icon: 'https://misskey-hub.net/patrons/6a652e0534ff4cb1836e7ce4968d76a7.jpg',
+	icon: 'https://assets.misskey-hub.net/patrons/6a652e0534ff4cb1836e7ce4968d76a7.jpg',
 }, {
 	name: 'かみらえっと',
-	icon: 'https://misskey-hub.net/patrons/be1326bda7d940a482f3758ffd9ffaf6.jpg',
+	icon: 'https://assets.misskey-hub.net/patrons/be1326bda7d940a482f3758ffd9ffaf6.jpg',
 }, {
 	name: 'へてて',
-	icon: 'https://misskey-hub.net/patrons/0431eacd7c6843d09de8ea9984307e86.jpg',
+	icon: 'https://assets.misskey-hub.net/patrons/0431eacd7c6843d09de8ea9984307e86.jpg',
 }, {
 	name: 'spinlock',
-	icon: 'https://misskey-hub.net/patrons/6a1cebc819d540a78bf20e9e3115baa8.jpg',
+	icon: 'https://assets.misskey-hub.net/patrons/6a1cebc819d540a78bf20e9e3115baa8.jpg',
 }, {
 	name: 'じゅくま',
-	icon: 'https://misskey-hub.net/patrons/3e56bdac69dd42f7a06e0f12cf2fc895.jpg',
+	icon: 'https://assets.misskey-hub.net/patrons/3e56bdac69dd42f7a06e0f12cf2fc895.jpg',
 }, {
 	name: '清遊あみ',
-	icon: 'https://misskey-hub.net/patrons/de25195b88e940a388388bea2e7637d8.jpg',
+	icon: 'https://assets.misskey-hub.net/patrons/de25195b88e940a388388bea2e7637d8.jpg',
 }, {
 	name: 'Nagi8410',
-	icon: 'https://misskey-hub.net/patrons/31b102ab4fc540ed806b0461575d38be.jpg',
+	icon: 'https://assets.misskey-hub.net/patrons/31b102ab4fc540ed806b0461575d38be.jpg',
 }, {
 	name: '山岡士郎',
-	icon: 'https://misskey-hub.net/patrons/84b9056341684266bb1eda3e680d094d.jpg',
+	icon: 'https://assets.misskey-hub.net/patrons/84b9056341684266bb1eda3e680d094d.jpg',
 }, {
 	name: 'よもやまたろう',
-	icon: 'https://misskey-hub.net/patrons/4273c9cce50d445f8f7d0f16113d6d7f.jpg',
+	icon: 'https://assets.misskey-hub.net/patrons/4273c9cce50d445f8f7d0f16113d6d7f.jpg',
 }, {
 	name: '花咲ももか',
-	icon: 'https://misskey-hub.net/patrons/8c9b2b9128cb4fee99f04bb4f86f2efa.jpg',
+	icon: 'https://assets.misskey-hub.net/patrons/8c9b2b9128cb4fee99f04bb4f86f2efa.jpg',
 }, {
 	name: 'カガミ',
-	icon: 'https://misskey-hub.net/patrons/226ea3a4617749548580ec2d9a263e24.jpg',
+	icon: 'https://assets.misskey-hub.net/patrons/226ea3a4617749548580ec2d9a263e24.jpg',
 }, {
 	name: 'フランギ・シュウ',
-	icon: 'https://misskey-hub.net/patrons/3016d37e35f3430b90420176c912d304.jpg',
+	icon: 'https://assets.misskey-hub.net/patrons/3016d37e35f3430b90420176c912d304.jpg',
 }, {
 	name: '百日紅',
-	icon: 'https://misskey-hub.net/patrons/302dce2898dd457ba03c3f7dc037900b.jpg',
+	icon: 'https://assets.misskey-hub.net/patrons/302dce2898dd457ba03c3f7dc037900b.jpg',
 }, {
 	name: 'taichan',
-	icon: 'https://misskey-hub.net/patrons/f981ab0159fb4e2c998e05f7263e1cd9.png',
+	icon: 'https://assets.misskey-hub.net/patrons/f981ab0159fb4e2c998e05f7263e1cd9.png',
 }];
 
 const patrons = [
diff --git a/packages/frontend/src/pages/share.vue b/packages/frontend/src/pages/share.vue
index 3e9cac9858..cb5acf3afa 100644
--- a/packages/frontend/src/pages/share.vue
+++ b/packages/frontend/src/pages/share.vue
@@ -30,7 +30,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-// SPECIFICATION: https://misskey-hub.net/docs/features/share-form.html
+// SPECIFICATION: https://misskey-hub.net/docs/for-users/features/share-form/
 
 import { ref, computed } from 'vue';
 import * as Misskey from 'misskey-js';
diff --git a/packages/frontend/src/ui/_common_/common.ts b/packages/frontend/src/ui/_common_/common.ts
index bfafe3dd96..b970ff1df4 100644
--- a/packages/frontend/src/ui/_common_/common.ts
+++ b/packages/frontend/src/ui/_common_/common.ts
@@ -101,7 +101,7 @@ export function openInstanceMenu(ev: MouseEvent) {
 		text: i18n.ts.help,
 		icon: 'ti ti-help-circle',
 		action: () => {
-			window.open('https://misskey-hub.net/help.html', '_blank', 'noopener');
+			window.open('https://misskey-hub.net/docs/for-users/', '_blank', 'noopener');
 		},
 	}, ($i) ? {
 		text: i18n.ts._initialTutorial.launchTutorial,

From 5eb944ecdeb0d65ec82b89522dfdf37d980bdb51 Mon Sep 17 00:00:00 2001
From: ikasoba <57828948+ikasoba@users.noreply.github.com>
Date: Fri, 22 Dec 2023 20:41:42 +0900
Subject: [PATCH 09/25] =?UTF-8?q?enhance:=20=E3=83=81=E3=83=A3=E3=83=B3?=
 =?UTF-8?q?=E3=83=8D=E3=83=AB=E3=81=AB=E6=96=B0=E8=A6=8F=E3=81=AE=E6=8A=95?=
 =?UTF-8?q?=E7=A8=BF=E3=81=8C=E3=81=82=E3=82=8B=E5=A0=B4=E5=90=88=E3=81=AB?=
 =?UTF-8?q?=E3=83=90=E3=83=83=E3=82=B8=E3=82=92=E8=A1=A8=E7=A4=BA=E3=81=95?=
 =?UTF-8?q?=E3=81=9B=E3=82=8B=20(#12690)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

* 多分できたかも

* 不要なpropsを削除

* 不要なimportを削除

* 縁を付けた

* 枠線の位置を端に寄せた

* やっぱり内側へ寄せることにした

* できたかも

* 修正

* 修正

* クラスにまとめた

* 微調整

* 直せたかも

* importを付け足し

* 多分できたかも

* Update channel.vue

* Update MkMenu.vue

* Update channel.vue

* Update CHANGELOG.md

---------

Co-authored-by: syuilo <Syuilotan@yahoo.co.jp>
---
 CHANGELOG.md                                  |  1 +
 .../src/components/MkChannelPreview.vue       | 98 +++++++++++++------
 packages/frontend/src/components/MkMenu.vue   | 64 ++++++++----
 packages/frontend/src/local-storage.ts        |  3 +-
 packages/frontend/src/pages/channel.vue       | 40 ++++++--
 packages/frontend/src/pages/timeline.vue      | 18 ++--
 6 files changed, 159 insertions(+), 65 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 95f435b7d7..d5ce885b9b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -69,6 +69,7 @@
 	- 逆に、MFMでコードハイライトを利用したい際は言語を明示的に指定する必要があります  
 	(例: ` ```js ` → Javascript, ` ```ais ` → AiScript)
 -	Enhance: 絵文字などのオートコンプリートでShift+Tabを押すと前の候補を選択できるように
+- Enhance: チャンネルに新規の投稿がある場合にバッジを表示させる
 - Fix: 「設定のバックアップ」で一部の項目がバックアップに含まれていなかった問題を修正
 - Fix: ウィジェットのジョブキューにて音声の発音方法変更に追従できていなかったのを修正 #12367
 - Fix: コードエディタが正しく表示されない問題を修正
diff --git a/packages/frontend/src/components/MkChannelPreview.vue b/packages/frontend/src/components/MkChannelPreview.vue
index 4512f2dd60..bf6504d6bf 100644
--- a/packages/frontend/src/components/MkChannelPreview.vue
+++ b/packages/frontend/src/components/MkChannelPreview.vue
@@ -4,49 +4,70 @@ SPDX-License-Identifier: AGPL-3.0-only
 -->
 
 <template>
-<MkA :to="`/channels/${channel.id}`" class="eftoefju _panel" tabindex="-1">
-	<div class="banner" :style="bannerStyle">
-		<div class="fade"></div>
-		<div class="name"><i class="ti ti-device-tv"></i> {{ channel.name }}</div>
-		<div v-if="channel.isSensitive" class="sensitiveIndicator">{{ i18n.ts.sensitive }}</div>
-		<div class="status">
-			<div>
-				<i class="ti ti-users ti-fw"></i>
-				<I18n :src="i18n.ts._channel.usersCount" tag="span" style="margin-left: 4px;">
-					<template #n>
-						<b>{{ channel.usersCount }}</b>
-					</template>
-				</I18n>
-			</div>
-			<div>
-				<i class="ti ti-pencil ti-fw"></i>
-				<I18n :src="i18n.ts._channel.notesCount" tag="span" style="margin-left: 4px;">
-					<template #n>
-						<b>{{ channel.notesCount }}</b>
-					</template>
-				</I18n>
+<div style="position: relative;">
+	<MkA :to="`/channels/${channel.id}`" class="eftoefju _panel" tabindex="-1" @click="updateLastReadedAt">
+		<div class="banner" :style="bannerStyle">
+			<div class="fade"></div>
+			<div class="name"><i class="ti ti-device-tv"></i> {{ channel.name }}</div>
+			<div v-if="channel.isSensitive" class="sensitiveIndicator">{{ i18n.ts.sensitive }}</div>
+			<div class="status">
+				<div>
+					<i class="ti ti-users ti-fw"></i>
+					<I18n :src="i18n.ts._channel.usersCount" tag="span" style="margin-left: 4px;">
+						<template #n>
+							<b>{{ channel.usersCount }}</b>
+						</template>
+					</I18n>
+				</div>
+				<div>
+					<i class="ti ti-pencil ti-fw"></i>
+					<I18n :src="i18n.ts._channel.notesCount" tag="span" style="margin-left: 4px;">
+						<template #n>
+							<b>{{ channel.notesCount }}</b>
+						</template>
+					</I18n>
+				</div>
 			</div>
 		</div>
-	</div>
-	<article v-if="channel.description">
-		<p :title="channel.description">{{ channel.description.length > 85 ? channel.description.slice(0, 85) + '…' : channel.description }}</p>
-	</article>
-	<footer>
-		<span v-if="channel.lastNotedAt">
-			{{ i18n.ts.updatedAt }}: <MkTime :time="channel.lastNotedAt"/>
-		</span>
-	</footer>
-</MkA>
+		<article v-if="channel.description">
+			<p :title="channel.description">{{ channel.description.length > 85 ? channel.description.slice(0, 85) + '…' : channel.description }}</p>
+		</article>
+		<footer>
+			<span v-if="channel.lastNotedAt">
+				{{ i18n.ts.updatedAt }}: <MkTime :time="channel.lastNotedAt"/>
+			</span>
+		</footer>
+	</MkA>
+	<div
+		v-if="channel.lastNotedAt && (channel.isFavorited || channel.isFollowing) && (!lastReadedAt || Date.parse(channel.lastNotedAt) > lastReadedAt)"
+		class="indicator"
+	></div>
+</div>
 </template>
 
 <script lang="ts" setup>
-import { computed } from 'vue';
+import { computed, ref, watch } from 'vue';
 import { i18n } from '@/i18n.js';
+import { miLocalStorage } from '@/local-storage.js';
 
 const props = defineProps<{
 	channel: Record<string, any>;
 }>();
 
+const getLastReadedAt = (): number | null => {
+	return miLocalStorage.getItemAsJson(`channelLastReadedAt:${props.channel.id}`) ?? null;
+};
+
+const lastReadedAt = ref(getLastReadedAt());
+
+watch(() => props.channel.id, () => {
+	lastReadedAt.value = getLastReadedAt();
+});
+
+const updateLastReadedAt = () => {
+	lastReadedAt.value = props.channel.lastNotedAt ? Date.parse(props.channel.lastNotedAt) : Date.now();
+};
+
 const bannerStyle = computed(() => {
 	if (props.channel.bannerUrl) {
 		return { backgroundImage: `url(${props.channel.bannerUrl})` };
@@ -170,4 +191,17 @@ const bannerStyle = computed(() => {
 	}
 }
 
+.indicator {
+	position: absolute;
+	top: 0;
+	right: 0;
+	transform: translate(25%, -25%);
+	background-color: var(--accent);
+	border: solid var(--bg) 4px;
+	border-radius: 100%;
+	width: 1.5rem;
+	height: 1.5rem;
+	aspect-ratio: 1 / 1;
+}
+
 </style>
diff --git a/packages/frontend/src/components/MkMenu.vue b/packages/frontend/src/components/MkMenu.vue
index 8e4b86f1c7..0a97cf3dba 100644
--- a/packages/frontend/src/components/MkMenu.vue
+++ b/packages/frontend/src/components/MkMenu.vue
@@ -15,7 +15,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 		<template v-for="(item, i) in items2">
 			<div v-if="item.type === 'divider'" role="separator" :class="$style.divider"></div>
 			<span v-else-if="item.type === 'label'" role="menuitem" :class="[$style.label, $style.item]">
-				<span>{{ item.text }}</span>
+				<span style="opacity: 0.7;">{{ item.text }}</span>
 			</span>
 			<span v-else-if="item.type === 'pending'" role="menuitem" :tabindex="i" :class="[$style.pending, $style.item]">
 				<span><MkEllipsis/></span>
@@ -23,32 +23,44 @@ SPDX-License-Identifier: AGPL-3.0-only
 			<MkA v-else-if="item.type === 'link'" role="menuitem" :to="item.to" :tabindex="i" class="_button" :class="$style.item" @click.passive="close(true)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
 				<i v-if="item.icon" class="ti-fw" :class="[$style.icon, item.icon]"></i>
 				<MkAvatar v-if="item.avatar" :user="item.avatar" :class="$style.avatar"/>
-				<span>{{ item.text }}</span>
-				<span v-if="item.indicate" :class="$style.indicator"><i class="_indicatorCircle"></i></span>
+				<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>
+				</div>
 			</MkA>
 			<a v-else-if="item.type === 'a'" role="menuitem" :href="item.href" :target="item.target" :download="item.download" :tabindex="i" class="_button" :class="$style.item" @click="close(true)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
 				<i v-if="item.icon" class="ti-fw" :class="[$style.icon, item.icon]"></i>
-				<span>{{ item.text }}</span>
-				<span v-if="item.indicate" :class="$style.indicator"><i class="_indicatorCircle"></i></span>
+				<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>
+				</div>
 			</a>
 			<button v-else-if="item.type === 'user'" role="menuitem" :tabindex="i" class="_button" :class="[$style.item, { [$style.active]: item.active }]" :disabled="item.active" @click="clicked(item.action, $event)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
 				<MkAvatar :user="item.user" :class="$style.avatar"/><MkUserName :user="item.user"/>
-				<span v-if="item.indicate" :class="$style.indicator"><i class="_indicatorCircle"></i></span>
+				<div v-if="item.indicate" :class="$style.item_content">
+					<span :class="$style.indicator"><i class="_indicatorCircle"></i></span>
+				</div>
 			</button>
 			<button v-else-if="item.type === 'switch'" role="menuitemcheckbox" :tabindex="i" class="_button" :class="[$style.item, $style.switch, { [$style.switchDisabled]: item.disabled } ]" @click="switchItem(item)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
 				<MkSwitchButton :class="$style.switchButton" :checked="item.ref" :disabled="item.disabled" @toggle="switchItem(item)"/>
-				<span :class="$style.switchText">{{ item.text }}</span>
+				<div :class="$style.item_content">
+					<span :class="[$style.item_content_text, $style.switchText]">{{ item.text }}</span>
+				</div>
 			</button>
 			<button v-else-if="item.type === 'parent'" class="_button" role="menuitem" :tabindex="i" :class="[$style.item, $style.parent, { [$style.childShowing]: childShowingItem === item }]" @mouseenter="preferClick ? null : showChildren(item, $event)" @click="!preferClick ? null : showChildren(item, $event)">
 				<i v-if="item.icon" class="ti-fw" :class="[$style.icon, item.icon]" style="pointer-events: none;"></i>
-				<span style="pointer-events: none;">{{ item.text }}</span>
-				<span :class="$style.caret" style="pointer-events: none;"><i class="ti ti-chevron-right ti-fw"></i></span>
+				<div :class="$style.item_content">
+					<span :class="$style.item_content_text" style="pointer-events: none;">{{ item.text }}</span>
+					<span :class="$style.caret" style="pointer-events: none;"><i class="ti ti-chevron-right ti-fw"></i></span>
+				</div>
 			</button>
 			<button v-else :tabindex="i" class="_button" role="menuitem" :class="[$style.item, { [$style.danger]: item.danger, [$style.active]: item.active }]" :disabled="item.active" @click="clicked(item.action, $event)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
 				<i v-if="item.icon" class="ti-fw" :class="[$style.icon, item.icon]"></i>
 				<MkAvatar v-if="item.avatar" :user="item.avatar" :class="$style.avatar"/>
-				<span>{{ item.text }}</span>
-				<span v-if="item.indicate" :class="$style.indicator"><i class="_indicatorCircle"></i></span>
+				<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>
+				</div>
 			</button>
 		</template>
 		<span v-if="items2.length === 0" :class="[$style.none, $style.item]">
@@ -228,6 +240,7 @@ onBeforeUnmount(() => {
 .root {
 	padding: 8px 0;
 	box-sizing: border-box;
+	max-width: 100vw;
 	min-width: 200px;
 	overflow: auto;
 	overscroll-behavior: contain;
@@ -267,7 +280,8 @@ onBeforeUnmount(() => {
 }
 
 .item {
-	display: block;
+	display: flex;
+	align-items: center;
 	position: relative;
 	padding: 5px 16px;
 	width: 100%;
@@ -340,10 +354,6 @@ onBeforeUnmount(() => {
 		pointer-events: none;
 		font-size: 0.7em;
 		padding-bottom: 4px;
-
-		> span {
-			opacity: 0.7;
-		}
 	}
 
 	&.pending {
@@ -373,6 +383,22 @@ onBeforeUnmount(() => {
 	}
 }
 
+.item_content {
+	width: 100%;
+	max-width: 100vw;
+	display: flex;
+	align-items: center;
+	justify-content: space-between;
+	gap: 8px;
+	text-overflow: ellipsis;
+}
+
+.item_content_text {
+	max-width: calc(100vw - 4rem);
+	text-overflow: ellipsis;
+	overflow: hidden;
+}
+
 .switch {
 	position: relative;
 	display: flex;
@@ -406,6 +432,7 @@ onBeforeUnmount(() => {
 
 .icon {
 	margin-right: 8px;
+	line-height: 1;
 }
 
 .caret {
@@ -419,9 +446,8 @@ onBeforeUnmount(() => {
 }
 
 .indicator {
-	position: absolute;
-	top: 5px;
-	left: 13px;
+	display: flex;
+	align-items: center;
 	color: var(--indicator);
 	font-size: 12px;
 	animation: blink 1s infinite;
diff --git a/packages/frontend/src/local-storage.ts b/packages/frontend/src/local-storage.ts
index 0d73885b68..1ef115978e 100644
--- a/packages/frontend/src/local-storage.ts
+++ b/packages/frontend/src/local-storage.ts
@@ -35,7 +35,8 @@ type Keys =
 	`themes:${string}` |
 	`aiscript:${string}` |
 	'lastEmojisFetchedAt' | // DEPRECATED, stored in indexeddb (13.9.0~)
-	'emojis' // DEPRECATED, stored in indexeddb (13.9.0~);
+	'emojis' | // DEPRECATED, stored in indexeddb (13.9.0~);
+	`channelLastReadedAt:${string}`
 
 export const miLocalStorage = {
 	getItem: (key: Keys): string | null => window.localStorage.getItem(key),
diff --git a/packages/frontend/src/pages/channel.vue b/packages/frontend/src/pages/channel.vue
index 698f7fa383..421895ea6c 100644
--- a/packages/frontend/src/pages/channel.vue
+++ b/packages/frontend/src/pages/channel.vue
@@ -12,7 +12,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 				<XChannelFollowButton :channel="channel" :full="true" :class="$style.subscribe"/>
 				<MkButton v-if="favorited" v-tooltip="i18n.ts.unfavorite" asLike class="button" rounded primary :class="$style.favorite" @click="unfavorite()"><i class="ti ti-star"></i></MkButton>
 				<MkButton v-else v-tooltip="i18n.ts.favorite" asLike class="button" rounded :class="$style.favorite" @click="favorite()"><i class="ti ti-star"></i></MkButton>
-				<div :style="{ backgroundImage: channel.bannerUrl ? `url(${channel.bannerUrl})` : null }" :class="$style.banner">
+				<div :style="{ backgroundImage: channel.bannerUrl ? `url(${channel.bannerUrl})` : undefined }" :class="$style.banner">
 					<div :class="$style.bannerStatus">
 						<div><i class="ti ti-users ti-fw"></i><I18n :src="i18n.ts._channel.usersCount" tag="span" style="margin-left: 4px;"><template #n><b>{{ channel.usersCount }}</b></template></I18n></div>
 						<div><i class="ti ti-pencil ti-fw"></i><I18n :src="i18n.ts._channel.notesCount" tag="span" style="margin-left: 4px;"><template #n><b>{{ channel.notesCount }}</b></template></I18n></div>
@@ -27,7 +27,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 			<MkFoldableSection>
 				<template #header><i class="ti ti-pin ti-fw" style="margin-right: 0.5em;"></i>{{ i18n.ts.pinnedNotes }}</template>
-				<div v-if="channel.pinnedNotes.length > 0" class="_gaps">
+				<div v-if="channel.pinnedNotes && channel.pinnedNotes.length > 0" class="_gaps">
 					<MkNote v-for="note in channel.pinnedNotes" :key="note.id" class="_panel" :note="note"/>
 				</div>
 			</MkFoldableSection>
@@ -38,7 +38,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 			<!-- スマホ・タブレットの場合、キーボードが表示されると投稿が見づらくなるので、デスクトップ場合のみ自動でフォーカスを当てる -->
 			<MkPostForm v-if="$i && defaultStore.reactiveState.showFixedPostFormInChannel.value" :channel="channel" class="post-form _panel" fixed :autofocus="deviceKind === 'desktop'"/>
 
-			<MkTimeline :key="channelId" src="channel" :channel="channelId" @before="before" @after="after"/>
+			<MkTimeline :key="channelId" src="channel" :channel="channelId" @before="before" @after="after" @note="miLocalStorage.setItemAsJson(`channelLastReadedAt:${channel.id}`, Date.now())"/>
 		</div>
 		<div v-else-if="tab === 'featured'">
 			<MkNotes :pagination="featuredPagination"/>
@@ -69,6 +69,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <script lang="ts" setup>
 import { computed, watch, ref } from 'vue';
+import * as Misskey from 'misskey-js';
 import MkPostForm from '@/components/MkPostForm.vue';
 import MkTimeline from '@/components/MkTimeline.vue';
 import XChannelFollowButton from '@/components/MkChannelFollowButton.vue';
@@ -89,6 +90,7 @@ import MkFoldableSection from '@/components/MkFoldableSection.vue';
 import { PageHeaderItem } from '@/types/page-header.js';
 import { isSupportShare } from '@/scripts/navigator.js';
 import copyToClipboard from '@/scripts/copy-to-clipboard.js';
+import { miLocalStorage } from '@/local-storage.js';
 
 const router = useRouter();
 
@@ -97,7 +99,7 @@ const props = defineProps<{
 }>();
 
 const tab = ref('overview');
-const channel = ref(null);
+const channel = ref<Misskey.entities.Channel | null>(null);
 const favorited = ref(false);
 const searchQuery = ref('');
 const searchPagination = ref();
@@ -114,14 +116,23 @@ watch(() => props.channelId, async () => {
 	channel.value = await os.api('channels/show', {
 		channelId: props.channelId,
 	});
-	favorited.value = channel.value.isFavorited;
+	favorited.value = channel.value.isFavorited ?? false;
 	if (favorited.value || channel.value.isFollowing) {
 		tab.value = 'timeline';
 	}
+
+	if ((favorited.value || channel.value.isFollowing) && channel.value.lastNotedAt) {
+		const lastReadedAt: number = miLocalStorage.getItemAsJson(`channelLastReadedAt:${channel.value.id}`) ?? 0;
+		const lastNotedAt = Date.parse(channel.value.lastNotedAt);
+
+		if (lastNotedAt > lastReadedAt) {
+			miLocalStorage.setItemAsJson(`channelLastReadedAt:${channel.value.id}`, lastNotedAt);
+		}
+	}
 }, { immediate: true });
 
 function edit() {
-	router.push(`/channels/${channel.value.id}/edit`);
+	router.push(`/channels/${channel.value?.id}/edit`);
 }
 
 function openPostForm() {
@@ -131,6 +142,8 @@ function openPostForm() {
 }
 
 function favorite() {
+	if (!channel.value) return;
+
 	os.apiWithDialog('channels/favorite', {
 		channelId: channel.value.id,
 	}).then(() => {
@@ -139,6 +152,8 @@ function favorite() {
 }
 
 async function unfavorite() {
+	if (!channel.value) return;
+
 	const confirm = await os.confirm({
 		type: 'warning',
 		text: i18n.ts.unfavoriteConfirm,
@@ -152,6 +167,8 @@ async function unfavorite() {
 }
 
 async function search() {
+	if (!channel.value) return;
+
 	const query = searchQuery.value.toString().trim();
 
 	if (query == null) return;
@@ -176,6 +193,10 @@ const headerActions = computed(() => {
 			icon: 'ti ti-link',
 			text: i18n.ts.copyUrl,
 			handler: async (): Promise<void> => {
+				if (!channel.value) {
+					console.warn('failed to copy channel URL. channel.value is null.');
+					return;
+				}
 				copyToClipboard(`${url}/channels/${channel.value.id}`);
 				os.success();
 			},
@@ -186,9 +207,14 @@ const headerActions = computed(() => {
 				icon: 'ti ti-share',
 				text: i18n.ts.share,
 				handler: async (): Promise<void> => {
+					if (!channel.value) {
+						console.warn('failed to share channel. channel.value is null.');
+						return;
+					}
+
 					navigator.share({
 						title: channel.value.name,
-						text: channel.value.description,
+						text: channel.value.description ?? undefined,
 						url: `${url}/channels/${channel.value.id}`,
 					});
 				},
diff --git a/packages/frontend/src/pages/timeline.vue b/packages/frontend/src/pages/timeline.vue
index d976463db4..1b24f98bdb 100644
--- a/packages/frontend/src/pages/timeline.vue
+++ b/packages/frontend/src/pages/timeline.vue
@@ -48,6 +48,7 @@ import { definePageMetadata } from '@/scripts/page-metadata.js';
 import { antennasCache, userListsCache } from '@/cache.js';
 import { deviceKind } from '@/scripts/device-kind.js';
 import { MenuItem } from '@/types/menu.js';
+import { miLocalStorage } from '@/local-storage.js';
 
 provide('shouldOmitHeaderTitle', true);
 
@@ -125,12 +126,17 @@ async function chooseChannel(ev: MouseEvent): Promise<void> {
 		limit: 100,
 	});
 	const items: MenuItem[] = [
-		...channels.map(channel => ({
-			type: 'link' as const,
-			text: channel.name,
-			indicate: channel.hasUnreadNote,
-			to: `/channels/${channel.id}`,
-		})),
+		...channels.map(channel => {
+			const lastReadedAt = miLocalStorage.getItemAsJson(`channelLastReadedAt:${channel.id}`) ?? null;
+			const hasUnreadNote = (lastReadedAt && channel.lastNotedAt) ? Date.parse(channel.lastNotedAt) > lastReadedAt : !!(!lastReadedAt && channel.lastNotedAt);
+
+			return {
+				type: 'link' as const,
+				text: channel.name,
+				indicate: hasUnreadNote,
+				to: `/channels/${channel.id}`,
+			};
+		}),
 		(channels.length === 0 ? undefined : { type: 'divider' }),
 		{
 			type: 'link' as const,

From 6b7a810b8e2b6efabada73b2969b310177e195db Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Fri, 22 Dec 2023 20:57:59 +0900
Subject: [PATCH 10/25] Update CHANGELOG.md

---
 CHANGELOG.md | 8 +++++++-
 1 file changed, 7 insertions(+), 1 deletion(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index d5ce885b9b..a2a444269c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -63,6 +63,7 @@
 - Enhance: ユーザー名、プロフィール、お知らせ、ページの編集画面でMFMや絵文字のオートコンプリートが使用できるように
 - Enhance: プロフィール、お知らせの編集画面でMFMのプレビューを表示できるように
 - Enhance: 絵文字の詳細ページに記載される情報を追加
+- Enhance: リアクションの表示幅制限を設定可能に
 - Enhance: Unicode 15.0のサポート
 - Enhance: コードブロックのハイライト機能を利用するには言語を明示的に指定させるように
 	- MFMでコードブロックを利用する際に意図しないハイライトが起こらないようになりました
@@ -70,6 +71,8 @@
 	(例: ` ```js ` → Javascript, ` ```ais ` → AiScript)
 -	Enhance: 絵文字などのオートコンプリートでShift+Tabを押すと前の候補を選択できるように
 - Enhance: チャンネルに新規の投稿がある場合にバッジを表示させる
+- Enhance: サウンド設定に「サウンドを出力しない」と「Misskeyがアクティブな時のみサウンドを出力する」を追加
+- Enhance: 設定したタグをトレンドに表示させないようにする項目を管理画面で設定できるように
 - Fix: 「設定のバックアップ」で一部の項目がバックアップに含まれていなかった問題を修正
 - Fix: ウィジェットのジョブキューにて音声の発音方法変更に追従できていなかったのを修正 #12367
 - Fix: コードエディタが正しく表示されない問題を修正
@@ -85,10 +88,14 @@
 - Fix: 投票のみ/画像のみの引用RNが、通知欄でただのRNとして判定されるバグを修正
 - Fix: CWをつけて引用RNしても、普通のRNとして扱われてしまうバグを修正しました。
 - Fix: 「画像が1枚のみのメディアリストの高さ」を「デフォルト」以外に設定していると、CWの中などに添付された画像が見られないバグを修正
+- Fix: DeepL TranslationのPro accountトグルスイッチが表示されていなかったのを修正
+- Fix: twitterの埋め込みカード内リンクからリンク先を開けない問題を修正
+- Fix: WebKitブラウザー上でも「デバイスの画面を常にオンにする」機能が効くように
 
 ### Server
 - Enhance: MFM `$[ruby ]` が他ソフトウェアと連合されるように
 - Enhance: Meilisearchを有効にした検索で、ユーザーのミュートやブロックを考慮するように
+- Enhance: カスタム絵文字のインポート時の動作を改善
 - Fix: 時間経過により無効化されたアンテナを再有効化したとき、サーバ再起動までその状況が反映されないのを修正 #12303
 - Fix: ロールタイムラインが保存されない問題を修正
 - Fix: api.jsonの生成ロジックを改善 #12402
@@ -125,7 +132,6 @@
 	- 例: `$[unixtime 1701356400]`
 - Enhance: プラグインでエラーが発生した場合のハンドリングを強化
 - Enhance: 細かなUIのブラッシュアップ
-- Enhance: サウンド設定に「サウンドを出力しない」と「Misskeyがアクティブな時のみサウンドを出力する」を追加
 - Fix: 効果音が再生されるとデバイスで再生している動画や音声が停止する問題を修正 #12339
 - Fix: デッキに表示されたチャンネルの表示先チャンネルを切り替えた際、即座に反映されない問題を修正 #12236
 - Fix: プラグインでノートの表示を書き換えられない問題を修正

From 9c0474935972aedc5151ae24a7b92b20768030ca Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Fri, 22 Dec 2023 20:59:28 +0900
Subject: [PATCH 11/25] 2023.12.0

---
 CHANGELOG.md | 2 +-
 package.json | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index a2a444269c..ddebf7d267 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -13,7 +13,7 @@
 
 -->
 
-## 2023.x.x (unreleased)
+## 2023.12.0
 
 ### Note
 - Node.js 20.10.0が最小要件になりました
diff --git a/package.json b/package.json
index 562c5ce407..d39b800a18 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
 	"name": "misskey",
-	"version": "2023.12.0-beta.6",
+	"version": "2023.12.0",
 	"codename": "nasubi",
 	"repository": {
 		"type": "git",

From 6254954957f74238e060f4d02ab5b391925ddbfe Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Fri, 22 Dec 2023 21:50:40 +0900
Subject: [PATCH 12/25] Update CHANGELOG.md

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

diff --git a/CHANGELOG.md b/CHANGELOG.md
index ddebf7d267..2c21684e77 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,8 +5,7 @@
 -
 
 ### Client
-- Fix: ページ一覧ページの表示がモバイル環境において崩れているのを修正
-- Fix: MFMでルビの中のテキストがnyaizeされない問題を修正
+- 
 
 ### Server
 -
@@ -91,6 +90,8 @@
 - Fix: DeepL TranslationのPro accountトグルスイッチが表示されていなかったのを修正
 - Fix: twitterの埋め込みカード内リンクからリンク先を開けない問題を修正
 - Fix: WebKitブラウザー上でも「デバイスの画面を常にオンにする」機能が効くように
+- Fix: ページ一覧ページの表示がモバイル環境において崩れているのを修正
+- Fix: MFMでルビの中のテキストがnyaizeされない問題を修正
 
 ### Server
 - Enhance: MFM `$[ruby ]` が他ソフトウェアと連合されるように

From b3c4f7eddc4d97e15077f1e9041c5abecc184afb Mon Sep 17 00:00:00 2001
From: Nya Candy <dev@candinya.com>
Date: Sat, 23 Dec 2023 10:00:14 +0800
Subject: [PATCH 13/25] fix: email verify enable logic (#12743)

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

diff --git a/packages/backend/src/core/EmailService.ts b/packages/backend/src/core/EmailService.ts
index f31cec2b3a..d4508d5313 100644
--- a/packages/backend/src/core/EmailService.ts
+++ b/packages/backend/src/core/EmailService.ts
@@ -167,18 +167,18 @@ export class EmailService {
 		const verifymailApi = meta.enableVerifymailApi && meta.verifymailAuthKey != null;
 		let validated;
 
-		if (meta.enableActiveEmailValidation && meta.verifymailAuthKey) {
+		if (meta.enableActiveEmailValidation) {
 			if (verifymailApi) {
 				validated = await this.verifyMail(emailAddress, meta.verifymailAuthKey);
 			} else {
-				validated = meta.enableActiveEmailValidation ? await validateEmail({
+				validated = await validateEmail({
 					email: emailAddress,
 					validateRegex: true,
 					validateMx: true,
 					validateTypo: false, // TLDを見ているみたいだけどclubとか弾かれるので
 					validateDisposable: true, // 捨てアドかどうかチェック
 					validateSMTP: false, // 日本だと25ポートが殆どのプロバイダーで塞がれていてタイムアウトになるので
-				}) : { valid: true, reason: null };
+				});
 			}
 		} else {
 			validated = { valid: true, reason: null };

From 5b5a537f567a7a2cbce008ac19aaaea372dd4695 Mon Sep 17 00:00:00 2001
From: GrapeApple0 <84321396+GrapeApple0@users.noreply.github.com>
Date: Sat, 23 Dec 2023 12:06:22 +0900
Subject: [PATCH 14/25] =?UTF-8?q?feat:=20=E7=99=BB=E9=8C=B2=E3=82=92?=
 =?UTF-8?q?=E6=8B=92=E5=90=A6=E3=81=99=E3=82=8B=E3=83=A1=E3=83=BC=E3=83=AB?=
 =?UTF-8?q?=E3=82=A2=E3=83=89=E3=83=AC=E3=82=B9=E3=81=AE=E3=83=89=E3=83=A1?=
 =?UTF-8?q?=E3=82=A4=E3=83=B3=E3=82=92=E6=89=8B=E5=8B=95=E3=81=A7=E8=A8=AD?=
 =?UTF-8?q?=E5=AE=9A=E3=81=A7=E3=81=8D=E3=82=8B=E3=82=88=E3=81=86=E3=81=AB?=
 =?UTF-8?q?=20(#12740)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

* feat: 使い捨てアドレスのドメインを手動で設定できるように

* Update CHANGELOG.md

* disposableEmailDomains -> bannedEmailDomains

* isBlockedHostを使うように
---
 CHANGELOG.md                                   |  1 +
 locales/index.d.ts                             |  1 +
 locales/ja-JP.yml                              |  1 +
 .../1703209889304-bannedEmailDomains.js        | 18 ++++++++++++++++++
 packages/backend/src/core/EmailService.ts      | 10 ++++++++--
 packages/backend/src/models/Meta.ts            |  7 +++++++
 .../src/server/api/endpoints/admin/meta.ts     |  9 +++++++++
 .../server/api/endpoints/admin/update-meta.ts  |  5 +++++
 .../src/components/MkSignupDialog.form.vue     |  4 +++-
 packages/frontend/src/pages/admin/security.vue | 15 +++++++++++++++
 10 files changed, 68 insertions(+), 3 deletions(-)
 create mode 100644 packages/backend/migration/1703209889304-bannedEmailDomains.js

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2c21684e77..71a90620e2 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -94,6 +94,7 @@
 - Fix: MFMでルビの中のテキストがnyaizeされない問題を修正
 
 ### Server
+- Feat: 使い捨てメールのドメインを手動で設定できるように
 - Enhance: MFM `$[ruby ]` が他ソフトウェアと連合されるように
 - Enhance: Meilisearchを有効にした検索で、ユーザーのミュートやブロックを考慮するように
 - Enhance: カスタム絵文字のインポート時の動作を改善
diff --git a/locales/index.d.ts b/locales/index.d.ts
index fd96fd7625..b3589082e1 100644
--- a/locales/index.d.ts
+++ b/locales/index.d.ts
@@ -1745,6 +1745,7 @@ export interface Locale {
         "disposable": string;
         "mx": string;
         "smtp": string;
+        "banned": string;
     };
     "_ffVisibility": {
         "public": string;
diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml
index 2c29bd20da..b59fb6e749 100644
--- a/locales/ja-JP.yml
+++ b/locales/ja-JP.yml
@@ -1652,6 +1652,7 @@ _emailUnavailable:
   disposable: "恒久的に使用可能なアドレスではありません"
   mx: "正しいメールサーバーではありません"
   smtp: "メールサーバーが応答しません"
+  banned: "このメールアドレスでは登録できません"
 
 _ffVisibility:
   public: "公開"
diff --git a/packages/backend/migration/1703209889304-bannedEmailDomains.js b/packages/backend/migration/1703209889304-bannedEmailDomains.js
new file mode 100644
index 0000000000..5dc99c138f
--- /dev/null
+++ b/packages/backend/migration/1703209889304-bannedEmailDomains.js
@@ -0,0 +1,18 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+export class bannedEmailDomains1703209889304 {
+		constructor() {
+				this.name = 'bannedEmailDomains1703209889304';
+		}
+
+		async up(queryRunner) {
+				await queryRunner.query(`ALTER TABLE "meta" ADD "bannedEmailDomains" character varying(1024) array NOT NULL DEFAULT '{}'`);
+		}
+
+		async down(queryRunner) {
+				await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "bannedEmailDomains"`);
+		}
+}
diff --git a/packages/backend/src/core/EmailService.ts b/packages/backend/src/core/EmailService.ts
index d4508d5313..6107b9601c 100644
--- a/packages/backend/src/core/EmailService.ts
+++ b/packages/backend/src/core/EmailService.ts
@@ -9,6 +9,7 @@ import { Inject, Injectable } from '@nestjs/common';
 import { validate as validateEmail } from 'deep-email-validator';
 import { SubOutputFormat } from 'deep-email-validator/dist/output/output.js';
 import { MetaService } from '@/core/MetaService.js';
+import { UtilityService } from '@/core/UtilityService.js';
 import { DI } from '@/di-symbols.js';
 import type { Config } from '@/config.js';
 import type Logger from '@/logger.js';
@@ -30,6 +31,7 @@ export class EmailService {
 
 		private metaService: MetaService,
 		private loggerService: LoggerService,
+		private utilityService: UtilityService,
 		private httpRequestService: HttpRequestService,
 	) {
 		this.logger = this.loggerService.getLogger('email');
@@ -155,7 +157,7 @@ export class EmailService {
 	@bindThis
 	public async validateEmailForAccount(emailAddress: string): Promise<{
 		available: boolean;
-		reason: null | 'used' | 'format' | 'disposable' | 'mx' | 'smtp';
+		reason: null | 'used' | 'format' | 'disposable' | 'mx' | 'smtp' | 'banned';
 	}> {
 		const meta = await this.metaService.fetch();
 
@@ -184,12 +186,16 @@ export class EmailService {
 			validated = { valid: true, reason: null };
 		}
 
-		const available = exist === 0 && validated.valid;
+		const emailDomain: string = emailAddress.split('@')[1];
+		const isBanned = this.utilityService.isBlockedHost(meta.bannedEmailDomains, emailDomain);
+
+		const available = exist === 0 && validated.valid && !isBanned;
 
 		return {
 			available,
 			reason: available ? null :
 			exist !== 0 ? 'used' :
+			isBanned ? 'banned' :
 			validated.reason === 'regex' ? 'format' :
 			validated.reason === 'disposable' ? 'disposable' :
 			validated.reason === 'mx' ? 'mx' :
diff --git a/packages/backend/src/models/Meta.ts b/packages/backend/src/models/Meta.ts
index 83e8962f5d..84ca762492 100644
--- a/packages/backend/src/models/Meta.ts
+++ b/packages/backend/src/models/Meta.ts
@@ -495,6 +495,13 @@ export class MiMeta {
 	})
 	public manifestJsonOverride: string;
 
+	@Column('varchar', {
+		length: 1024,
+		array: true,
+		default: '{}',
+	})
+	public bannedEmailDomains: string[];
+
 	@Column('varchar', {
 		length: 1024, array: true, default: '{ "admin", "administrator", "root", "system", "maintainer", "host", "mod", "moderator", "owner", "superuser", "staff", "auth", "i", "me", "everyone", "all", "mention", "mentions", "example", "user", "users", "account", "accounts", "official", "help", "helps", "support", "supports", "info", "information", "informations", "announce", "announces", "announcement", "announcements", "notice", "notification", "notifications", "dev", "developer", "developers", "tech", "misskey" }',
 	})
diff --git a/packages/backend/src/server/api/endpoints/admin/meta.ts b/packages/backend/src/server/api/endpoints/admin/meta.ts
index 07912154bd..6f8494d1d0 100644
--- a/packages/backend/src/server/api/endpoints/admin/meta.ts
+++ b/packages/backend/src/server/api/endpoints/admin/meta.ts
@@ -145,6 +145,14 @@ export const meta = {
 					type: 'string',
 				},
 			},
+			bannedEmailDomains: {
+				type: 'array',
+				optional: true, nullable: false,
+				items: {
+					type: 'string',
+					optional: false, nullable: false,
+				},
+			},
 			preservedUsernames: {
 				type: 'array',
 				optional: false, nullable: false,
@@ -513,6 +521,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 				enableChartsForFederatedInstances: instance.enableChartsForFederatedInstances,
 				enableServerMachineStats: instance.enableServerMachineStats,
 				enableIdenticonGeneration: instance.enableIdenticonGeneration,
+				bannedEmailDomains: instance.bannedEmailDomains,
 				policies: { ...DEFAULT_POLICIES, ...instance.policies },
 				manifestJsonOverride: instance.manifestJsonOverride,
 				enableFanoutTimeline: instance.enableFanoutTimeline,
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 293a95a9a4..5f9de0523e 100644
--- a/packages/backend/src/server/api/endpoints/admin/update-meta.ts
+++ b/packages/backend/src/server/api/endpoints/admin/update-meta.ts
@@ -122,6 +122,7 @@ export const paramDef = {
 		enableServerMachineStats: { type: 'boolean' },
 		enableIdenticonGeneration: { type: 'boolean' },
 		serverRules: { type: 'array', items: { type: 'string' } },
+		bannedEmailDomains: { type: 'array', items: { type: 'string' } },
 		preservedUsernames: { type: 'array', items: { type: 'string' } },
 		manifestJsonOverride: { type: 'string' },
 		enableFanoutTimeline: { type: 'boolean' },
@@ -526,6 +527,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 				set.notesPerOneAd = ps.notesPerOneAd;
 			}
 
+			if (ps.bannedEmailDomains !== undefined) {
+				set.bannedEmailDomains = ps.bannedEmailDomains;
+			}
+
 			const before = await this.metaService.fetch(true);
 
 			await this.metaService.update(set);
diff --git a/packages/frontend/src/components/MkSignupDialog.form.vue b/packages/frontend/src/components/MkSignupDialog.form.vue
index dd05a44e04..f171e449c8 100644
--- a/packages/frontend/src/components/MkSignupDialog.form.vue
+++ b/packages/frontend/src/components/MkSignupDialog.form.vue
@@ -38,6 +38,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 					<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>
@@ -110,7 +111,7 @@ const retypedPassword = ref<string>('');
 const invitationCode = ref<string>('');
 const email = ref('');
 const usernameState = ref<null | 'wait' | 'ok' | 'unavailable' | 'error' | 'invalid-format' | 'min-range' | 'max-range'>(null);
-const emailState = ref<null | 'wait' | 'ok' | 'unavailable:used' | 'unavailable:format' | 'unavailable:disposable' | 'unavailable:mx' | 'unavailable:smtp' | 'unavailable' | 'error'>(null);
+const emailState = ref<null | 'wait' | 'ok' | 'unavailable:used' | 'unavailable:format' | 'unavailable:disposable' | 'unavailable:banned' | 'unavailable:mx' | 'unavailable:smtp' | 'unavailable' | 'error'>(null);
 const passwordStrength = ref<'' | 'low' | 'medium' | 'high'>('');
 const passwordRetypeState = ref<null | 'match' | 'not-match'>(null);
 const submitting = ref<boolean>(false);
@@ -209,6 +210,7 @@ function onChangeEmail(): void {
 			result.reason === 'used' ? 'unavailable:used' :
 			result.reason === 'format' ? 'unavailable:format' :
 			result.reason === 'disposable' ? 'unavailable:disposable' :
+			result.reason === 'banned' ? 'unavailable:banned' :
 			result.reason === 'mx' ? 'unavailable:mx' :
 			result.reason === 'smtp' ? 'unavailable:smtp' :
 			'unavailable';
diff --git a/packages/frontend/src/pages/admin/security.vue b/packages/frontend/src/pages/admin/security.vue
index 9835591fa8..bda29cee58 100644
--- a/packages/frontend/src/pages/admin/security.vue
+++ b/packages/frontend/src/pages/admin/security.vue
@@ -83,6 +83,17 @@ SPDX-License-Identifier: AGPL-3.0-only
 					</div>
 				</MkFolder>
 
+				<MkFolder>
+					<template #label>Banned Email Domains</template>
+
+					<div class="_gaps_m">
+						<MkTextarea v-model="bannedEmailDomains">
+							<template #label>Banned Email Domains List</template>
+						</MkTextarea>
+						<MkButton primary @click="save"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton>
+					</div>
+				</MkFolder>
+
 				<MkFolder>
 					<template #label>Log IP address</template>
 					<template v-if="enableIpLogging" #suffix>Enabled</template>
@@ -124,6 +135,7 @@ import FormSuspense from '@/components/form/suspense.vue';
 import MkRange from '@/components/MkRange.vue';
 import MkInput from '@/components/MkInput.vue';
 import MkButton from '@/components/MkButton.vue';
+import MkTextarea from '@/components/MkTextarea.vue';
 import * as os from '@/os.js';
 import { fetchInstance } from '@/instance.js';
 import { i18n } from '@/i18n.js';
@@ -141,6 +153,7 @@ const enableIpLogging = ref<boolean>(false);
 const enableActiveEmailValidation = ref<boolean>(false);
 const enableVerifymailApi = ref<boolean>(false);
 const verifymailAuthKey = ref<string | null>(null);
+const bannedEmailDomains = ref<string>('');
 
 async function init() {
 	const meta = await os.api('admin/meta');
@@ -161,6 +174,7 @@ async function init() {
 	enableActiveEmailValidation.value = meta.enableActiveEmailValidation;
 	enableVerifymailApi.value = meta.enableVerifymailApi;
 	verifymailAuthKey.value = meta.verifymailAuthKey;
+	bannedEmailDomains.value = meta.bannedEmailDomains.join('\n');
 }
 
 function save() {
@@ -180,6 +194,7 @@ function save() {
 		enableActiveEmailValidation: enableActiveEmailValidation.value,
 		enableVerifymailApi: enableVerifymailApi.value,
 		verifymailAuthKey: verifymailAuthKey.value,
+		bannedEmailDomains: bannedEmailDomains.value.split('\n'),
 	}).then(() => {
 		fetchInstance();
 	});

From 2c7d07bca6a6b6a3390674c5fcc3b618092b2507 Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Sat, 23 Dec 2023 12:15:10 +0900
Subject: [PATCH 15/25] Update CHANGELOG.md

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

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 71a90620e2..d131077bcb 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -31,6 +31,7 @@
 - Feat: メールアドレスの認証にverifymail.ioを使えるように (cherry-pick from https://github.com/TeamNijimiss/misskey/commit/971ba07a44550f68d2ba31c62066db2d43a0caed)
 - Feat: モデレーターがユーザーのアイコンもしくはバナー画像を未設定状態にできる機能を追加 (cherry-pick from https://github.com/TeamNijimiss/misskey/commit/e0eb5a752f6e5616d6312bb7c9790302f9dbff83)
 - Feat: TL上からノートが見えなくなるワードミュートであるハードミュートを追加
+- Enhance: 指定したドメインのメールアドレスの登録を弾くことができるように
 - Enhance: 公開ロールにアサインされたときに通知が作成されるように
 - Enhance: アイコンデコレーションを複数設定できるように
 - Enhance: アイコンデコレーションの位置を微調整できるように
@@ -94,7 +95,6 @@
 - Fix: MFMでルビの中のテキストがnyaizeされない問題を修正
 
 ### Server
-- Feat: 使い捨てメールのドメインを手動で設定できるように
 - Enhance: MFM `$[ruby ]` が他ソフトウェアと連合されるように
 - Enhance: Meilisearchを有効にした検索で、ユーザーのミュートやブロックを考慮するように
 - Enhance: カスタム絵文字のインポート時の動作を改善

From 98734af9a7d9365608a8e84995eb34fdfdfb1dfe 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, 23 Dec 2023 14:30:39 +0900
Subject: [PATCH 16/25] =?UTF-8?q?fix:=202023.12.0=E3=81=AENote=E3=81=AE?=
 =?UTF-8?q?=E4=B8=80=E9=83=A8=E6=96=87=E8=A8=80=E3=82=92=E4=BF=AE=E6=AD=A3?=
 =?UTF-8?q?=20(#12754)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

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

diff --git a/CHANGELOG.md b/CHANGELOG.md
index d131077bcb..0f864acfee 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -20,12 +20,12 @@
 - 絵文字ピッカーにピン留め表示する絵文字設定が「リアクション用」と「絵文字入力用」に分かれました。以前の設定は「リアクション用」として使用されます。  
 
 	**影響:**  
-	それにより、投稿フォームから表示される絵文字ピッカーのピン留め絵文字がリセットされたように感じるかもしれません(新設された投稿用のピン留め絵文字が使われるため)。   
+	それにより、投稿フォームから表示される絵文字ピッカーのピン留め絵文字がリセットされたように感じるかもしれません(新設された"ピン留め(全般)"の設定が使われるため)。   
 	投稿用のピン留め絵文字をアップデート前の状態にするには、以下の手順で操作します。
 
 	1. 「設定」メニューに移動し、「絵文字ピッカー」タブを選択します。
 	2. 「ピン留 (全般)」のタブを選択します。
-	3. 「リアクション設定からコピーする」ボタンを押すことで、アップデート前の状態に戻すことができます。
+	3. 「リアクション設定から上書きする」ボタンを押すことで、アップデート前の状態に戻すことができます。
 
 ### General
 - Feat: メールアドレスの認証にverifymail.ioを使えるように (cherry-pick from https://github.com/TeamNijimiss/misskey/commit/971ba07a44550f68d2ba31c62066db2d43a0caed)

From 6e4894c1656d283906b679866923fddab2b146bf Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Sat, 23 Dec 2023 14:39:01 +0900
Subject: [PATCH 17/25] lint

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

diff --git a/packages/backend/src/core/EmailService.ts b/packages/backend/src/core/EmailService.ts
index 6107b9601c..3a61e353f1 100644
--- a/packages/backend/src/core/EmailService.ts
+++ b/packages/backend/src/core/EmailService.ts
@@ -166,11 +166,10 @@ export class EmailService {
 			email: emailAddress,
 		});
 
-		const verifymailApi = meta.enableVerifymailApi && meta.verifymailAuthKey != null;
 		let validated;
 
 		if (meta.enableActiveEmailValidation) {
-			if (verifymailApi) {
+			if (meta.enableVerifymailApi && meta.verifymailAuthKey != null) {
 				validated = await this.verifyMail(emailAddress, meta.verifymailAuthKey);
 			} else {
 				validated = await validateEmail({

From 1716c6562c86c20aaba734b427913fc6a6abd67c 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, 23 Dec 2023 15:32:31 +0900
Subject: [PATCH 18/25] =?UTF-8?q?fix:=20.npmrc=E3=81=AB=E3=82=88=E3=82=8Ap?=
 =?UTF-8?q?ackage.json=E8=A8=98=E8=BC=89=E3=81=AEnode=E3=83=90=E3=83=BC?=
 =?UTF-8?q?=E3=82=B8=E3=83=A7=E3=83=B3=E3=81=AB=E6=BA=80=E3=81=9F=E3=81=AA?=
 =?UTF-8?q?=E3=81=84=E5=A0=B4=E5=90=88=E3=81=AF=E3=83=93=E3=83=AB=E3=83=89?=
 =?UTF-8?q?=E3=81=AB=E5=A4=B1=E6=95=97=E3=81=99=E3=82=8B=E3=82=88=E3=81=86?=
 =?UTF-8?q?=E3=81=AB=E3=81=99=E3=82=8B=20(#12755)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .npmrc | 1 +
 1 file changed, 1 insertion(+)
 create mode 100644 .npmrc

diff --git a/.npmrc b/.npmrc
new file mode 100644
index 0000000000..c42da845b4
--- /dev/null
+++ b/.npmrc
@@ -0,0 +1 @@
+engine-strict = true

From 30cf5c3ab09717829a2e49a6afe14fe6478140dc Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E7=9A=90=E6=9C=88=E3=81=AA=E3=81=B5=20=28Nafu=20Satsuki?=
 =?UTF-8?q?=29?= <satsuki@nafusoft.dev>
Date: Sat, 23 Dec 2023 15:32:53 +0900
Subject: [PATCH 19/25] =?UTF-8?q?chore(frontend):=20API=E8=A8=AD=E5=AE=9A?=
 =?UTF-8?q?=E9=A0=85=E7=9B=AE=E3=81=AE=E5=90=8D=E5=89=8D=E3=82=92=E3=81=8D?=
 =?UTF-8?q?=E3=81=A1=E3=82=93=E3=81=A8=E3=82=B5=E3=83=BC=E3=83=93=E3=82=B9?=
 =?UTF-8?q?=E3=81=AE=E5=90=8D=E5=89=8D=E3=81=A7=E8=A1=A8=E8=A8=98=E3=81=99?=
 =?UTF-8?q?=E3=82=8B=20(#12753)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

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

diff --git a/packages/frontend/src/pages/admin/security.vue b/packages/frontend/src/pages/admin/security.vue
index bda29cee58..7070157ca9 100644
--- a/packages/frontend/src/pages/admin/security.vue
+++ b/packages/frontend/src/pages/admin/security.vue
@@ -74,11 +74,11 @@ SPDX-License-Identifier: AGPL-3.0-only
 							<template #label>Enable</template>
 						</MkSwitch>
 						<MkSwitch v-model="enableVerifymailApi" @update:modelValue="save">
-							<template #label>Use Verifymail API</template>
+							<template #label>Use Verifymail.io API</template>
 						</MkSwitch>
 						<MkInput v-model="verifymailAuthKey" @update:modelValue="save">
 							<template #prefix><i class="ti ti-key"></i></template>
-							<template #label>Verifymail API Auth Key</template>
+							<template #label>Verifymail.io API Auth Key</template>
 						</MkInput>
 					</div>
 				</MkFolder>

From 59b47b862340f772a51e5d8e8cbaab5f1c4e53f5 Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Sat, 23 Dec 2023 16:40:31 +0900
Subject: [PATCH 20/25] Update CHANGELOG.md

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

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0f864acfee..c7afb101db 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -15,7 +15,7 @@
 ## 2023.12.0
 
 ### Note
-- Node.js 20.10.0が最小要件になりました
+- 依存関係の更新に伴い、Node.js 20.10.0が最小要件になりました
 - 絵文字の追加辞書を既にインストールしている場合は、お手数ですが再インストールのほどお願いします
 - 絵文字ピッカーにピン留め表示する絵文字設定が「リアクション用」と「絵文字入力用」に分かれました。以前の設定は「リアクション用」として使用されます。  
 

From 8caf2b0a4ac771d4568f1549bdce850de2af7777 Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Sat, 23 Dec 2023 16:43:06 +0900
Subject: [PATCH 21/25] New Crowdin updates (#12748)

* New translations ja-jp.yml (Korean)

* New translations ja-jp.yml (French)

* New translations ja-jp.yml (Chinese Traditional)
---
 locales/fr-FR.yml | 1 +
 locales/ko-KR.yml | 1 +
 locales/zh-TW.yml | 1 +
 3 files changed, 3 insertions(+)

diff --git a/locales/fr-FR.yml b/locales/fr-FR.yml
index e12b508617..43cc1d45b6 100644
--- a/locales/fr-FR.yml
+++ b/locales/fr-FR.yml
@@ -1917,6 +1917,7 @@ _notification:
     pollEnded: "Sondages se cloturant"
     receiveFollowRequest: "Demande d'abonnement reçue"
     followRequestAccepted: "Demande d'abonnement acceptée"
+    roleAssigned: "Rôle reçu"
     achievementEarned: "Accomplissement"
     app: "Notifications provenant des apps"
   _actions:
diff --git a/locales/ko-KR.yml b/locales/ko-KR.yml
index d8efa7f04e..6cdcc2c246 100644
--- a/locales/ko-KR.yml
+++ b/locales/ko-KR.yml
@@ -2193,6 +2193,7 @@ _notification:
     pollEnded: "투표가 종료됨"
     receiveFollowRequest: "팔로우 요청을 받았을 때"
     followRequestAccepted: "팔로우 요청이 승인되었을 때"
+    roleAssigned: "역할이 부여 됨"
     achievementEarned: "도전 과제 획득"
     app: "연동된 앱을 통한 알림"
   _actions:
diff --git a/locales/zh-TW.yml b/locales/zh-TW.yml
index d05691d42e..51ba42e66c 100644
--- a/locales/zh-TW.yml
+++ b/locales/zh-TW.yml
@@ -2193,6 +2193,7 @@ _notification:
     pollEnded: "問卷調查結束"
     receiveFollowRequest: "已收到追隨請求"
     followRequestAccepted: "追隨請求已接受"
+    roleAssigned: "已授予角色"
     achievementEarned: "獲得成就"
     app: "應用程式通知"
   _actions:

From f43599552fb5764aa3121b083e441d3946c72cd8 Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Sat, 23 Dec 2023 17:54:29 +0900
Subject: [PATCH 22/25] =?UTF-8?q?fix(backend):=20renote=E5=88=A4=E5=AE=9A?=
 =?UTF-8?q?=E3=81=8C=E3=81=8A=E3=81=8B=E3=81=97=E3=81=84?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

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

diff --git a/packages/backend/src/core/NoteCreateService.ts b/packages/backend/src/core/NoteCreateService.ts
index 54493612b8..c4fc51847b 100644
--- a/packages/backend/src/core/NoteCreateService.ts
+++ b/packages/backend/src/core/NoteCreateService.ts
@@ -293,7 +293,7 @@ export class NoteCreateService implements OnApplicationShutdown {
 		}
 
 		// Check blocking
-		if (this.isQuote(data)) {
+		if (data.renote && data.text == null && data.poll == null && (data.files == null || data.files.length === 0)) {
 			if (data.renote.userHost === null) {
 				if (data.renote.userId !== user.id) {
 					const blocked = await this.userBlockingService.checkBlocked(data.renote.userId, user.id);

From e852f4b60d48edc5b28e6db104ca6a88dd678740 Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Sat, 23 Dec 2023 17:55:27 +0900
Subject: [PATCH 23/25] =?UTF-8?q?Revert=20"fix(backend):=20renote=E5=88=A4?=
 =?UTF-8?q?=E5=AE=9A=E3=81=8C=E3=81=8A=E3=81=8B=E3=81=97=E3=81=84"?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

This reverts commit f43599552fb5764aa3121b083e441d3946c72cd8.
---
 packages/backend/src/core/NoteCreateService.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/packages/backend/src/core/NoteCreateService.ts b/packages/backend/src/core/NoteCreateService.ts
index c4fc51847b..54493612b8 100644
--- a/packages/backend/src/core/NoteCreateService.ts
+++ b/packages/backend/src/core/NoteCreateService.ts
@@ -293,7 +293,7 @@ export class NoteCreateService implements OnApplicationShutdown {
 		}
 
 		// Check blocking
-		if (data.renote && data.text == null && data.poll == null && (data.files == null || data.files.length === 0)) {
+		if (this.isQuote(data)) {
 			if (data.renote.userHost === null) {
 				if (data.renote.userId !== user.id) {
 					const blocked = await this.userBlockingService.checkBlocked(data.renote.userId, user.id);

From 2f425aa03f705bc886711bc3b61f6fdd5f014f0b Mon Sep 17 00:00:00 2001
From: anatawa12 <anatawa12@icloud.com>
Date: Sat, 23 Dec 2023 17:55:34 +0900
Subject: [PATCH 24/25] =?UTF-8?q?fix:=20=E3=83=96=E3=83=AD=E3=83=83?=
 =?UTF-8?q?=E3=82=AF=E3=81=95=E3=82=8C=E3=81=A6=E3=81=A6=E3=82=82pure=20RN?=
 =?UTF-8?q?=E3=81=A7=E3=81=8D=E3=82=8B=20(#12758)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

なぜかわからないけど元々Quoteはできるようなのでそれに戻しました
---
 packages/backend/src/core/NoteCreateService.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/packages/backend/src/core/NoteCreateService.ts b/packages/backend/src/core/NoteCreateService.ts
index 54493612b8..2bdff872ad 100644
--- a/packages/backend/src/core/NoteCreateService.ts
+++ b/packages/backend/src/core/NoteCreateService.ts
@@ -293,7 +293,7 @@ export class NoteCreateService implements OnApplicationShutdown {
 		}
 
 		// Check blocking
-		if (this.isQuote(data)) {
+		if (data.renote && !this.isQuote(data)) {
 			if (data.renote.userHost === null) {
 				if (data.renote.userId !== user.id) {
 					const blocked = await this.userBlockingService.checkBlocked(data.renote.userId, user.id);

From 471c8ec0509741cd1c813535aa0e751f85b64a7c Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Sat, 23 Dec 2023 19:59:27 +0900
Subject: [PATCH 25/25] Update CHANGELOG.md

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

diff --git a/CHANGELOG.md b/CHANGELOG.md
index c7afb101db..ac31bc0d28 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -73,6 +73,7 @@
 - Enhance: チャンネルに新規の投稿がある場合にバッジを表示させる
 - Enhance: サウンド設定に「サウンドを出力しない」と「Misskeyがアクティブな時のみサウンドを出力する」を追加
 - Enhance: 設定したタグをトレンドに表示させないようにする項目を管理画面で設定できるように
+- Enhance: 絵文字ピッカーのカテゴリに「/」を入れることでフォルダ分け表示できるように
 - Fix: 「設定のバックアップ」で一部の項目がバックアップに含まれていなかった問題を修正
 - Fix: ウィジェットのジョブキューにて音声の発音方法変更に追従できていなかったのを修正 #12367
 - Fix: コードエディタが正しく表示されない問題を修正
@@ -243,7 +244,6 @@
 ### Client
 - Enhance: TLの返信表示オプションを記憶するように
 - Enhance: 投稿されてから時間が経過しているノートであることを視覚的に分かりやすく
-- Feat: 絵文字ピッカーのカテゴリに「/」を入れることでフォルダ分け表示できるように
 
 ### Server
 - Enhance: タイムライン取得時のパフォーマンスを向上