From 268b7aeb3f03c57c6a34e0461d1eb4d54cf56392 Mon Sep 17 00:00:00 2001
From: Lhcfl <Lhcfl@outlook.com>
Date: Wed, 10 Apr 2024 17:16:25 +0800
Subject: [PATCH 001/110] refactor: Fix type errors of mfm.ts

---
 packages/client/src/components/mfm.ts | 229 +++++++++++++++-----------
 1 file changed, 137 insertions(+), 92 deletions(-)

diff --git a/packages/client/src/components/mfm.ts b/packages/client/src/components/mfm.ts
index 0800e8270b..f2b100a207 100644
--- a/packages/client/src/components/mfm.ts
+++ b/packages/client/src/components/mfm.ts
@@ -1,6 +1,6 @@
 import { defineComponent, h } from "vue";
 import * as mfm from "mfm-js";
-import type { VNode, PropType } from "vue";
+import type { PropType, VNodeArrayChildren } from "vue";
 import MkUrl from "@/components/global/MkUrl.vue";
 import MkLink from "@/components/MkLink.vue";
 import MkMention from "@/components/MkMention.vue";
@@ -30,11 +30,12 @@ export default defineComponent({
 			default: false,
 		},
 		author: {
-			type: Object,
+			type: Object as PropType<entities.User>,
 			default: null,
 		},
+		// TODO: This variable is not used in the code and may be removed
 		i: {
-			type: Object,
+			type: Object as PropType<entities.User>,
 			default: null,
 		},
 		customEmojis: {
@@ -58,14 +59,16 @@ export default defineComponent({
 
 		const ast = (isPlain ? mfm.parseSimple : mfm.parse)(this.text);
 
-		const validTime = (t: string | null | undefined) => {
+		const validTime = (t: string | null | undefined | boolean) => {
 			if (t == null) return null;
+			if (typeof t !== "string") return null;
 			return t.match(/^[0-9.]+s$/) ? t : null;
 		};
 
-		const validNumber = (n: string | null | undefined) => {
+		const validNumber = (n: string | null | undefined | boolean) => {
 			if (n == null) return null;
-			const parsed = parseFloat(n);
+			if (typeof n !== "string") return null;
+			const parsed = Number.parseFloat(n);
 			return !Number.isNaN(parsed) && Number.isFinite(parsed) && parsed > 0;
 		};
 		// const validEase = (e: string | null | undefined) => {
@@ -77,13 +80,13 @@ export default defineComponent({
 
 		const genEl = (ast: mfm.MfmNode[]) =>
 			concat(
-				ast.map((token, index): VNode[] => {
+				ast.map((token, index): VNodeArrayChildren => {
 					switch (token.type) {
 						case "text": {
 							const text = token.props.text.replace(/(\r\n|\n|\r)/g, "\n");
 
 							if (!this.plain) {
-								const res = [];
+								const res: VNodeArrayChildren = [];
 								for (const t of text.split("\n")) {
 									res.push(h("br"));
 									res.push(t);
@@ -104,18 +107,20 @@ export default defineComponent({
 						}
 
 						case "italic": {
-							return h(
-								"i",
-								{
-									style: "font-style: oblique;",
-								},
-								genEl(token.children),
-							);
+							return [
+								h(
+									"i",
+									{
+										style: "font-style: oblique;",
+									},
+									genEl(token.children),
+								),
+							];
 						}
 
 						case "fn": {
 							// TODO: CSSを文字列で組み立てていくと token.props.args.~~~ 経由でCSSインジェクションできるのでよしなにやる
-							let style: string;
+							let style: string | null = null;
 							switch (token.props.name) {
 								case "tada": {
 									const speed = validTime(token.props.args.speed) || "1s";
@@ -188,7 +193,7 @@ export default defineComponent({
 									if (reducedMotion()) {
 										return genEl(token.children);
 									}
-									return h(MkSparkle, {}, genEl(token.children));
+									return [h(MkSparkle, {}, genEl(token.children))];
 								}
 								case "fade": {
 									const direction = token.props.args.out
@@ -211,31 +216,37 @@ export default defineComponent({
 									break;
 								}
 								case "x2": {
-									return h(
-										"span",
-										{
-											class: "mfm-x2",
-										},
-										genEl(token.children),
-									);
+									return [
+										h(
+											"span",
+											{
+												class: "mfm-x2",
+											},
+											genEl(token.children),
+										),
+									];
 								}
 								case "x3": {
-									return h(
-										"span",
-										{
-											class: "mfm-x3",
-										},
-										genEl(token.children),
-									);
+									return [
+										h(
+											"span",
+											{
+												class: "mfm-x3",
+											},
+											genEl(token.children),
+										),
+									];
 								}
 								case "x4": {
-									return h(
-										"span",
-										{
-											class: "mfm-x4",
-										},
-										genEl(token.children),
-									);
+									return [
+										h(
+											"span",
+											{
+												class: "mfm-x4",
+											},
+											genEl(token.children),
+										),
+									];
 								}
 								case "font": {
 									const family = token.props.args.serif
@@ -255,13 +266,15 @@ export default defineComponent({
 									break;
 								}
 								case "blur": {
-									return h(
-										"span",
-										{
-											class: "_blur_text",
-										},
-										genEl(token.children),
-									);
+									return [
+										h(
+											"span",
+											{
+												class: "_blur_text",
+											},
+											genEl(token.children),
+										),
+									];
 								}
 								case "rotate": {
 									const rotate = token.props.args.x
@@ -269,77 +282,105 @@ export default defineComponent({
 										: token.props.args.y
 											? "perspective(128px) rotateY"
 											: "rotate";
-									const degrees = parseFloat(token.props.args.deg ?? "90");
+									const degrees = Number.parseFloat(
+										token.props.args.deg.toString() ?? "90",
+									);
 									style = `transform: ${rotate}(${degrees}deg); transform-origin: center center;`;
 									break;
 								}
 								case "position": {
-									const x = parseFloat(token.props.args.x ?? "0");
-									const y = parseFloat(token.props.args.y ?? "0");
+									const x = Number.parseFloat(
+										token.props.args.x.toString() ?? "0",
+									);
+									const y = Number.parseFloat(
+										token.props.args.y.toString() ?? "0",
+									);
 									style = `transform: translateX(${x}em) translateY(${y}em);`;
 									break;
 								}
 								case "crop": {
-									const top = parseFloat(token.props.args.top ?? "0");
-									const right = parseFloat(token.props.args.right ?? "0");
-									const bottom = parseFloat(token.props.args.bottom ?? "0");
-									const left = parseFloat(token.props.args.left ?? "0");
+									const top = Number.parseFloat(
+										token.props.args.top.toString() ?? "0",
+									);
+									const right = Number.parseFloat(
+										token.props.args.right.toString() ?? "0",
+									);
+									const bottom = Number.parseFloat(
+										token.props.args.bottom.toString() ?? "0",
+									);
+									const left = Number.parseFloat(
+										token.props.args.left.toString() ?? "0",
+									);
 									style = `clip-path: inset(${top}% ${right}% ${bottom}% ${left}%);`;
 									break;
 								}
 								case "scale": {
-									const x = Math.min(parseFloat(token.props.args.x ?? "1"), 5);
-									const y = Math.min(parseFloat(token.props.args.y ?? "1"), 5);
+									const x = Math.min(
+										Number.parseFloat(token.props.args.x.toString() ?? "1"),
+										5,
+									);
+									const y = Math.min(
+										Number.parseFloat(token.props.args.y.toString() ?? "1"),
+										5,
+									);
 									style = `transform: scale(${x}, ${y});`;
 									break;
 								}
 								case "fg": {
 									let color = token.props.args.color;
-									if (!/^[0-9a-f]{3,6}$/i.test(color)) color = "f00";
+									if (!/^[0-9a-f]{3,6}$/i.test(color.toString())) color = "f00";
 									style = `color: #${color};`;
 									break;
 								}
 								case "bg": {
 									let color = token.props.args.color;
-									if (!/^[0-9a-f]{3,6}$/i.test(color)) color = "f00";
+									if (!/^[0-9a-f]{3,6}$/i.test(color.toString())) color = "f00";
 									style = `background-color: #${color};`;
 									break;
 								}
 								case "small": {
-									return h(
-										"small",
-										{
-											style: "opacity: 0.7;",
-										},
-										genEl(token.children),
-									);
+									return [
+										h(
+											"small",
+											{
+												style: "opacity: 0.7;",
+											},
+											genEl(token.children),
+										),
+									];
 								}
 								case "center": {
-									return h(
-										"div",
-										{
-											style: "text-align: center;",
-										},
-										genEl(token.children),
-									);
+									return [
+										h(
+											"div",
+											{
+												style: "text-align: center;",
+											},
+											genEl(token.children),
+										),
+									];
 								}
 							}
 							if (style == null) {
-								return h("span", {}, [
-									"$[",
-									token.props.name,
-									" ",
-									...genEl(token.children),
-									"]",
-								]);
+								return [
+									h("span", {}, [
+										"$[",
+										token.props.name,
+										" ",
+										...genEl(token.children),
+										"]",
+									]),
+								];
 							} else {
-								return h(
-									"span",
-									{
-										style: `display: inline-block;${style}`,
-									},
-									genEl(token.children),
-								);
+								return [
+									h(
+										"span",
+										{
+											style: `display: inline-block;${style}`,
+										},
+										genEl(token.children),
+									),
+								];
 							}
 						}
 
@@ -425,7 +466,7 @@ export default defineComponent({
 								h(MkCode, {
 									key: Math.random(),
 									code: token.props.code,
-									lang: token.props.lang,
+									lang: token.props.lang ?? undefined,
 								}),
 							];
 						}
@@ -506,13 +547,15 @@ export default defineComponent({
 								const ast2 = (isPlain ? mfm.parseSimple : mfm.parse)(
 									token.props.content.slice(0, -6) + sentinel,
 								);
+								function isMfmText(n: mfm.MfmNode): n is mfm.MfmText {
+									return n.type === "text";
+								}
+								const txtNode = ast2[ast2.length - 1];
 								if (
-									ast2[ast2.length - 1].type === "text" &&
-									ast2[ast2.length - 1].props.text.endsWith(sentinel)
+									isMfmText(txtNode) &&
+									txtNode.props.text.endsWith(sentinel)
 								) {
-									ast2[ast2.length - 1].props.text = ast2[
-										ast2.length - 1
-									].props.text.slice(0, -1);
+									txtNode.props.text = txtNode.props.text.slice(0, -1);
 								} else {
 									// I don't think this scope is reachable
 									console.warn(
@@ -554,8 +597,10 @@ export default defineComponent({
 						}
 
 						default: {
-							console.error("unrecognized ast type:", token.type);
-
+							console.error(
+								"unrecognized ast type:",
+								(token as { type: never }).type,
+							);
 							return [];
 						}
 					}

From 23145c61af00349061a312f7f86edce79d037a22 Mon Sep 17 00:00:00 2001
From: Lhcfl <Lhcfl@outlook.com>
Date: Wed, 10 Apr 2024 17:55:52 +0800
Subject: [PATCH 002/110] refactor: fix type of MkAbuseReport

---
 packages/backend/src/misc/schema.ts           |  2 +
 .../models/repositories/abuse-user-report.ts  |  6 +-
 .../src/models/schema/abuse-user-report.ts    | 69 +++++++++++++++++++
 .../api/endpoints/admin/abuse-user-reports.ts | 63 +----------------
 .../client/src/components/MkAbuseReport.vue   |  5 +-
 packages/client/src/pages/admin/abuses.vue    |  5 +-
 packages/firefish-js/src/api.types.ts         | 14 +++-
 packages/firefish-js/src/entities.ts          | 31 +++++----
 8 files changed, 114 insertions(+), 81 deletions(-)
 create mode 100644 packages/backend/src/models/schema/abuse-user-report.ts

diff --git a/packages/backend/src/misc/schema.ts b/packages/backend/src/misc/schema.ts
index c793d3b2ed..73600832ce 100644
--- a/packages/backend/src/misc/schema.ts
+++ b/packages/backend/src/misc/schema.ts
@@ -33,8 +33,10 @@ import { packedGalleryPostSchema } from "@/models/schema/gallery-post.js";
 import { packedEmojiSchema } from "@/models/schema/emoji.js";
 import { packedNoteEdit } from "@/models/schema/note-edit.js";
 import { packedNoteFileSchema } from "@/models/schema/note-file.js";
+import { packedAbuseUserReportSchema } from "@/models/schema/abuse-user-report.js";
 
 export const refs = {
+	AbuseUserReport: packedAbuseUserReportSchema,
 	UserLite: packedUserLiteSchema,
 	UserDetailedNotMeOnly: packedUserDetailedNotMeOnlySchema,
 	MeDetailedOnly: packedMeDetailedOnlySchema,
diff --git a/packages/backend/src/models/repositories/abuse-user-report.ts b/packages/backend/src/models/repositories/abuse-user-report.ts
index 16ce159955..b8d953d052 100644
--- a/packages/backend/src/models/repositories/abuse-user-report.ts
+++ b/packages/backend/src/models/repositories/abuse-user-report.ts
@@ -2,6 +2,7 @@ import { db } from "@/db/postgre.js";
 import { Users } from "../index.js";
 import { AbuseUserReport } from "@/models/entities/abuse-user-report.js";
 import { awaitAll } from "@/prelude/await-all.js";
+import type { Packed } from "@/misc/schema.js";
 
 export const AbuseUserReportRepository = db
 	.getRepository(AbuseUserReport)
@@ -10,7 +11,7 @@ export const AbuseUserReportRepository = db
 			const report =
 				typeof src === "object" ? src : await this.findOneByOrFail({ id: src });
 
-			return await awaitAll({
+			const packed: Packed<"AbuseUserReport"> = await awaitAll({
 				id: report.id,
 				createdAt: report.createdAt.toISOString(),
 				comment: report.comment,
@@ -31,9 +32,10 @@ export const AbuseUserReportRepository = db
 					: null,
 				forwarded: report.forwarded,
 			});
+			return packed;
 		},
 
-		packMany(reports: any[]) {
+		packMany(reports: (AbuseUserReport["id"] | AbuseUserReport)[]) {
 			return Promise.all(reports.map((x) => this.pack(x)));
 		},
 	});
diff --git a/packages/backend/src/models/schema/abuse-user-report.ts b/packages/backend/src/models/schema/abuse-user-report.ts
new file mode 100644
index 0000000000..47e56c7415
--- /dev/null
+++ b/packages/backend/src/models/schema/abuse-user-report.ts
@@ -0,0 +1,69 @@
+export const packedAbuseUserReportSchema = {
+	type: "object",
+	properties: {
+		id: {
+			type: "string",
+			optional: false,
+			nullable: false,
+			format: "id",
+			example: "xxxxxxxxxx",
+		},
+		createdAt: {
+			type: "string",
+			optional: false,
+			nullable: false,
+			format: "date-time",
+		},
+		comment: {
+			type: "string",
+			optional: false,
+			nullable: false,
+		},
+		resolved: {
+			type: "boolean",
+			optional: false,
+			nullable: false,
+		},
+		reporterId: {
+			type: "string",
+			optional: false,
+			nullable: false,
+			format: "id",
+		},
+		targetUserId: {
+			type: "string",
+			optional: false,
+			nullable: false,
+			format: "id",
+		},
+		assigneeId: {
+			type: "string",
+			optional: false,
+			nullable: true,
+			format: "id",
+		},
+		reporter: {
+			type: "object",
+			optional: false,
+			nullable: false,
+			ref: "UserDetailed",
+		},
+		targetUser: {
+			type: "object",
+			optional: false,
+			nullable: false,
+			ref: "UserDetailed",
+		},
+		assignee: {
+			type: "object",
+			optional: true,
+			nullable: true,
+			ref: "UserDetailed",
+		},
+		forwarded: {
+			type: "boolean",
+			optional: false,
+			nullable: false,
+		},
+	},
+} as const;
diff --git a/packages/backend/src/server/api/endpoints/admin/abuse-user-reports.ts b/packages/backend/src/server/api/endpoints/admin/abuse-user-reports.ts
index 78034917f0..4063af5c5c 100644
--- a/packages/backend/src/server/api/endpoints/admin/abuse-user-reports.ts
+++ b/packages/backend/src/server/api/endpoints/admin/abuse-user-reports.ts
@@ -16,68 +16,7 @@ export const meta = {
 			type: "object",
 			optional: false,
 			nullable: false,
-			properties: {
-				id: {
-					type: "string",
-					nullable: false,
-					optional: false,
-					format: "id",
-					example: "xxxxxxxxxx",
-				},
-				createdAt: {
-					type: "string",
-					nullable: false,
-					optional: false,
-					format: "date-time",
-				},
-				comment: {
-					type: "string",
-					nullable: false,
-					optional: false,
-				},
-				resolved: {
-					type: "boolean",
-					nullable: false,
-					optional: false,
-					example: false,
-				},
-				reporterId: {
-					type: "string",
-					nullable: false,
-					optional: false,
-					format: "id",
-				},
-				targetUserId: {
-					type: "string",
-					nullable: false,
-					optional: false,
-					format: "id",
-				},
-				assigneeId: {
-					type: "string",
-					nullable: true,
-					optional: false,
-					format: "id",
-				},
-				reporter: {
-					type: "object",
-					nullable: false,
-					optional: false,
-					ref: "User",
-				},
-				targetUser: {
-					type: "object",
-					nullable: false,
-					optional: false,
-					ref: "User",
-				},
-				assignee: {
-					type: "object",
-					nullable: true,
-					optional: true,
-					ref: "User",
-				},
-			},
+			ref: "AbuseUserReport",
 		},
 	},
 } as const;
diff --git a/packages/client/src/components/MkAbuseReport.vue b/packages/client/src/components/MkAbuseReport.vue
index 5536523948..26b91fae2e 100644
--- a/packages/client/src/components/MkAbuseReport.vue
+++ b/packages/client/src/components/MkAbuseReport.vue
@@ -72,13 +72,14 @@ import MkSwitch from "@/components/form/switch.vue";
 import MkKeyValue from "@/components/MkKeyValue.vue";
 import * as os from "@/os";
 import { i18n } from "@/i18n";
+import type { entities } from "firefish-js";
 
 const props = defineProps<{
-	report: any;
+	report: entities.AbuseUserReport;
 }>();
 
 const emit = defineEmits<{
-	(ev: "resolved", reportId: string): void;
+	resolved: [reportId: string];
 }>();
 
 const forward = ref(props.report.forwarded);
diff --git a/packages/client/src/pages/admin/abuses.vue b/packages/client/src/pages/admin/abuses.vue
index 174b4d5713..f94e24cf7c 100644
--- a/packages/client/src/pages/admin/abuses.vue
+++ b/packages/client/src/pages/admin/abuses.vue
@@ -99,12 +99,13 @@ import XAbuseReport from "@/components/MkAbuseReport.vue";
 import { i18n } from "@/i18n";
 import { definePageMetadata } from "@/scripts/page-metadata";
 import icon from "@/scripts/icon";
+import type { entities } from "firefish-js";
 
 const reports = ref<InstanceType<typeof MkPagination>>();
 
 const state = ref("unresolved");
-const reporterOrigin = ref("combined");
-const targetUserOrigin = ref("combined");
+const reporterOrigin = ref<entities.OriginType>("combined");
+const targetUserOrigin = ref<entities.OriginType>("combined");
 // const searchUsername = ref("");
 // const searchHost = ref("");
 
diff --git a/packages/firefish-js/src/api.types.ts b/packages/firefish-js/src/api.types.ts
index d3564573f0..81e9a4a72c 100644
--- a/packages/firefish-js/src/api.types.ts
+++ b/packages/firefish-js/src/api.types.ts
@@ -1,4 +1,5 @@
 import type {
+	AbuseUserReport,
 	Ad,
 	Announcement,
 	Antenna,
@@ -68,7 +69,18 @@ type NoteSubmitReq = {
 
 export type Endpoints = {
 	// admin
-	"admin/abuse-user-reports": { req: TODO; res: TODO };
+	"admin/abuse-user-reports": {
+		req: {
+			limit?: number;
+			sinceId?: AbuseUserReport["id"];
+			untilId?: AbuseUserReport["id"];
+			state?: string;
+			reporterOrigin?: OriginType;
+			targetUserOrigin?: OriginType;
+			forwarded?: boolean;
+		};
+		res: AbuseUserReport[];
+	};
 	"admin/delete-all-files-of-a-user": {
 		req: { userId: User["id"] };
 		res: null;
diff --git a/packages/firefish-js/src/entities.ts b/packages/firefish-js/src/entities.ts
index ce940a1481..8501d6c51f 100644
--- a/packages/firefish-js/src/entities.ts
+++ b/packages/firefish-js/src/entities.ts
@@ -18,10 +18,7 @@ export type UserLite = {
 	avatarBlurhash: string;
 	alsoKnownAs: string[];
 	movedToUri: any;
-	emojis: {
-		name: string;
-		url: string;
-	}[];
+	emojis: EmojiLite[];
 	instance?: {
 		name: Instance["name"];
 		softwareName: Instance["softwareName"];
@@ -171,10 +168,7 @@ export type Note = {
 			votes: number;
 		}[];
 	};
-	emojis: {
-		name: string;
-		url: string;
-	}[];
+	emojis: EmojiLite[];
 	uri?: string;
 	url?: string;
 	updatedAt?: DateString;
@@ -191,10 +185,7 @@ export type NoteEdit = {
 	updatedAt: string;
 	fileIds: DriveFile["id"][];
 	files: DriveFile[];
-	emojis: {
-		name: string;
-		url: string;
-	}[];
+	emojis: EmojiLite[];
 };
 
 export type NoteReaction = {
@@ -325,6 +316,8 @@ export type EmojiLite = {
 	id: string;
 	name: string;
 	url: string;
+	width: number | null;
+	height: number | null;
 };
 
 export type LiteInstanceMetadata = {
@@ -547,3 +540,17 @@ export type UserSorting =
 	| "+updatedAt"
 	| "-updatedAt";
 export type OriginType = "combined" | "local" | "remote";
+
+export type AbuseUserReport = {
+	id: string;
+	createdAt: DateString;
+	comment: string;
+	resolved: boolean;
+	reporterId: User["id"];
+	targetUserId: User["id"];
+	assigneeId: User["id"] | null;
+	reporter: UserDetailed;
+	targetUser: UserDetailed;
+	assignee?: UserDetailed | null;
+	forwarded: boolean;
+};

From b6baded2e3aabe9217e72b7f7ad2452d3793027b Mon Sep 17 00:00:00 2001
From: Lhcfl <Lhcfl@outlook.com>
Date: Wed, 10 Apr 2024 17:58:02 +0800
Subject: [PATCH 003/110] refactor: fix MkAcct type

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

diff --git a/packages/client/src/components/global/MkAcct.vue b/packages/client/src/components/global/MkAcct.vue
index 02112d4481..dff995f878 100644
--- a/packages/client/src/components/global/MkAcct.vue
+++ b/packages/client/src/components/global/MkAcct.vue
@@ -16,7 +16,7 @@ import { host as hostRaw } from "@/config";
 import { defaultStore } from "@/store";
 
 defineProps<{
-	user: entities.UserDetailed;
+	user: entities.UserLite;
 	detail?: boolean;
 }>();
 

From 6ac6a4cfa93c5758c1fed5aace0eb1a036f0e0c0 Mon Sep 17 00:00:00 2001
From: Lhcfl <Lhcfl@outlook.com>
Date: Wed, 10 Apr 2024 20:10:43 +0800
Subject: [PATCH 004/110] refactor: fix type errors of MkAnnouncements

---
 packages/client/src/components/MkActiveUsersHeatmap.vue | 8 ++++----
 packages/client/src/components/MkAnnouncement.vue       | 5 +++--
 packages/client/src/components/MkModal.vue              | 2 +-
 packages/firefish-js/src/entities.ts                    | 2 ++
 4 files changed, 10 insertions(+), 7 deletions(-)

diff --git a/packages/client/src/components/MkActiveUsersHeatmap.vue b/packages/client/src/components/MkActiveUsersHeatmap.vue
index 58eb42f5a6..8099e812f4 100644
--- a/packages/client/src/components/MkActiveUsersHeatmap.vue
+++ b/packages/client/src/components/MkActiveUsersHeatmap.vue
@@ -18,8 +18,8 @@ import { initChart } from "@/scripts/init-chart";
 
 initChart();
 
-const rootEl = shallowRef<HTMLDivElement>();
-const chartEl = shallowRef<HTMLCanvasElement>();
+const rootEl = shallowRef<HTMLDivElement | null>(null);
+const chartEl = shallowRef<HTMLCanvasElement | null>(null);
 const now = new Date();
 let chartInstance: Chart | null = null;
 const fetching = ref(true);
@@ -33,8 +33,8 @@ async function renderActiveUsersChart() {
 		chartInstance.destroy();
 	}
 
-	const wide = rootEl.value.offsetWidth > 700;
-	const narrow = rootEl.value.offsetWidth < 400;
+	const wide = rootEl.value!.offsetWidth > 700;
+	const narrow = rootEl.value!.offsetWidth < 400;
 
 	const weeks = wide ? 50 : narrow ? 10 : 25;
 	const chartLimit = 7 * weeks;
diff --git a/packages/client/src/components/MkAnnouncement.vue b/packages/client/src/components/MkAnnouncement.vue
index 4f26cd8bac..0aaa519cb0 100644
--- a/packages/client/src/components/MkAnnouncement.vue
+++ b/packages/client/src/components/MkAnnouncement.vue
@@ -35,9 +35,10 @@ import MkSparkle from "@/components/MkSparkle.vue";
 import MkButton from "@/components/MkButton.vue";
 import { i18n } from "@/i18n";
 import * as os from "@/os";
+import type { entities } from "firefish-js";
 
 const props = defineProps<{
-	announcement: Announcement;
+	announcement: entities.Announcement;
 }>();
 
 const { id, text, title, imageUrl, isGoodNews } = props.announcement;
@@ -45,7 +46,7 @@ const { id, text, title, imageUrl, isGoodNews } = props.announcement;
 const modal = shallowRef<InstanceType<typeof MkModal>>();
 
 const gotIt = () => {
-	modal.value.close();
+	modal.value!.close();
 	os.api("i/read-announcement", { announcementId: id });
 };
 </script>
diff --git a/packages/client/src/components/MkModal.vue b/packages/client/src/components/MkModal.vue
index a55525c632..6a2897cebf 100644
--- a/packages/client/src/components/MkModal.vue
+++ b/packages/client/src/components/MkModal.vue
@@ -190,7 +190,7 @@ const transitionDuration = computed(() =>
 let contentClicking = false;
 
 const focusedElement = document.activeElement;
-function close(_ev, opts: { useSendAnimation?: boolean } = {}) {
+function close(_ev?, opts: { useSendAnimation?: boolean } = {}) {
 	// removeEventListener("popstate", close);
 	// if (props.preferType == "dialog") {
 	// 	history.forward();
diff --git a/packages/firefish-js/src/entities.ts b/packages/firefish-js/src/entities.ts
index 8501d6c51f..086bf26193 100644
--- a/packages/firefish-js/src/entities.ts
+++ b/packages/firefish-js/src/entities.ts
@@ -422,6 +422,8 @@ export type Announcement = {
 	title: string;
 	imageUrl: string | null;
 	isRead?: boolean;
+	isGoodNews?: boolean;
+	showPopUp?: boolean;
 };
 
 export type Antenna = {

From 73537ec6fa3db65bbb72ce17b8fe34514ce06ff2 Mon Sep 17 00:00:00 2001
From: Lhcfl <Lhcfl@outlook.com>
Date: Wed, 10 Apr 2024 20:36:20 +0800
Subject: [PATCH 005/110] fix type errors of MkAutoComplete

---
 packages/client/src/components/MkAutocomplete.vue | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/packages/client/src/components/MkAutocomplete.vue b/packages/client/src/components/MkAutocomplete.vue
index 9a8582f19a..332785b467 100644
--- a/packages/client/src/components/MkAutocomplete.vue
+++ b/packages/client/src/components/MkAutocomplete.vue
@@ -62,7 +62,7 @@
 				<span v-else class="emoji">{{ emoji.emoji }}</span>
 				<span
 					class="name"
-					v-html="emoji.name.replace(q, `<b>${q}</b>`)"
+					v-html="q ? emoji.name.replace(q, `<b>${q}</b>`) : emoji.name"
 				></span>
 				<span v-if="emoji.aliasOf" class="alias"
 					>({{ emoji.aliasOf }})</span
@@ -107,7 +107,7 @@ interface EmojiDef {
 	emoji: string;
 	name: string;
 	aliasOf?: string;
-	url?: string;
+	url: string;
 	isCustomEmoji?: boolean;
 }
 

From 1a1d8177720159251716a6f64e51c3f605612610 Mon Sep 17 00:00:00 2001
From: Lhcfl <Lhcfl@outlook.com>
Date: Wed, 10 Apr 2024 20:37:34 +0800
Subject: [PATCH 006/110] fix type errors of MkAvatars

---
 packages/client/src/components/MkAvatars.vue | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/packages/client/src/components/MkAvatars.vue b/packages/client/src/components/MkAvatars.vue
index d92eee22c1..2b2029a8b5 100644
--- a/packages/client/src/components/MkAvatars.vue
+++ b/packages/client/src/components/MkAvatars.vue
@@ -9,12 +9,13 @@
 <script lang="ts" setup>
 import { onMounted, ref } from "vue";
 import * as os from "@/os";
+import type { entities } from "firefish-js";
 
 const props = defineProps<{
 	userIds: string[];
 }>();
 
-const users = ref([]);
+const users = ref<entities.UserDetailed[]>([]);
 
 onMounted(async () => {
 	users.value = await os.api("users/show", {

From b59186f09375f337710f3def901fb67b8ed21fcf Mon Sep 17 00:00:00 2001
From: Lhcfl <Lhcfl@outlook.com>
Date: Wed, 10 Apr 2024 20:41:31 +0800
Subject: [PATCH 007/110] fix type errors of MkButton

---
 packages/client/src/components/MkButton.vue   | 20 +++++++++++++------
 packages/client/src/components/global/MkA.vue |  2 +-
 2 files changed, 15 insertions(+), 7 deletions(-)

diff --git a/packages/client/src/components/MkButton.vue b/packages/client/src/components/MkButton.vue
index 48864be62a..a0ff747afc 100644
--- a/packages/client/src/components/MkButton.vue
+++ b/packages/client/src/components/MkButton.vue
@@ -47,7 +47,7 @@ const props = defineProps<{
 }>();
 
 const emit = defineEmits<{
-	(ev: "click", payload: MouseEvent): void;
+	click: [payload: MouseEvent];
 }>();
 
 const el = ref<HTMLElement | null>(null);
@@ -61,11 +61,19 @@ onMounted(() => {
 	}
 });
 
-function distance(p, q): number {
+function distance(
+	p: { x: number; y: number },
+	q: { x: number; y: number },
+): number {
 	return Math.hypot(p.x - q.x, p.y - q.y);
 }
 
-function calcCircleScale(boxW, boxH, circleCenterX, circleCenterY): number {
+function calcCircleScale(
+	boxW: number,
+	boxH: number,
+	circleCenterX: number,
+	circleCenterY: number,
+): number {
 	const origin = { x: circleCenterX, y: circleCenterY };
 	const dist1 = distance({ x: 0, y: 0 }, origin);
 	const dist2 = distance({ x: boxW, y: 0 }, origin);
@@ -79,8 +87,8 @@ function onMousedown(evt: MouseEvent): void {
 	const rect = target.getBoundingClientRect();
 
 	const ripple = document.createElement("div");
-	ripple.style.top = (evt.clientY - rect.top - 1).toString() + "px";
-	ripple.style.left = (evt.clientX - rect.left - 1).toString() + "px";
+	ripple.style.top = `${(evt.clientY - rect.top - 1).toString()}px`;
+	ripple.style.left = `${(evt.clientX - rect.left - 1).toString()}px`;
 
 	ripples.value!.appendChild(ripple);
 
@@ -97,7 +105,7 @@ function onMousedown(evt: MouseEvent): void {
 	vibrate(10);
 
 	window.setTimeout(() => {
-		ripple.style.transform = "scale(" + scale / 2 + ")";
+		ripple.style.transform = `scale(${scale / 2})`;
 	}, 1);
 	window.setTimeout(() => {
 		ripple.style.transition = "all 1s ease";
diff --git a/packages/client/src/components/global/MkA.vue b/packages/client/src/components/global/MkA.vue
index fbe5472a24..b85774dced 100644
--- a/packages/client/src/components/global/MkA.vue
+++ b/packages/client/src/components/global/MkA.vue
@@ -22,7 +22,7 @@ import icon from "@/scripts/icon";
 
 const props = withDefaults(
 	defineProps<{
-		to: string;
+		to?: string;
 		activeClass?: null | string;
 		behavior?: null | "window" | "browser" | "modalWindow";
 	}>(),

From c1155f169a79d863aec38a99ef2ab34420d7dc77 Mon Sep 17 00:00:00 2001
From: Lhcfl <Lhcfl@outlook.com>
Date: Wed, 10 Apr 2024 20:42:48 +0800
Subject: [PATCH 008/110] fix type warnings of MkCaptcha

---
 packages/client/src/components/MkCaptcha.vue | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/packages/client/src/components/MkCaptcha.vue b/packages/client/src/components/MkCaptcha.vue
index 146c512fb8..208f2b627a 100644
--- a/packages/client/src/components/MkCaptcha.vue
+++ b/packages/client/src/components/MkCaptcha.vue
@@ -50,7 +50,7 @@ const props = defineProps<{
 }>();
 
 const emit = defineEmits<{
-	(ev: "update:modelValue", v: string | null): void;
+	"update:modelValue": [v: string | null];
 }>();
 
 const available = ref(false);
@@ -93,6 +93,7 @@ if (loaded) {
 				src: src.value,
 			}),
 		)
+	// biome-ignore lint/suspicious/noAssignInExpressions: assign it intentially
 	).addEventListener("load", () => (available.value = true));
 }
 

From f7f7959ba67744919bd4d74fd0580e5cad717925 Mon Sep 17 00:00:00 2001
From: Lhcfl <Lhcfl@outlook.com>
Date: Wed, 10 Apr 2024 22:24:16 +0800
Subject: [PATCH 009/110] refactor: fix waring for MkCaptcha

---
 packages/client/src/components/MkCaptcha.vue | 5 +++--
 1 file changed, 3 insertions(+), 2 deletions(-)

diff --git a/packages/client/src/components/MkCaptcha.vue b/packages/client/src/components/MkCaptcha.vue
index 208f2b627a..135eee8e90 100644
--- a/packages/client/src/components/MkCaptcha.vue
+++ b/packages/client/src/components/MkCaptcha.vue
@@ -93,8 +93,9 @@ if (loaded) {
 				src: src.value,
 			}),
 		)
-	// biome-ignore lint/suspicious/noAssignInExpressions: assign it intentially
-	).addEventListener("load", () => (available.value = true));
+	)
+		// biome-ignore lint/suspicious/noAssignInExpressions: assign it intentially
+		.addEventListener("load", () => (available.value = true));
 }
 
 function reset() {

From 17a945b8b1558859cb3273a98ecc78bd5e90dc58 Mon Sep 17 00:00:00 2001
From: Lhcfl <Lhcfl@outlook.com>
Date: Wed, 10 Apr 2024 22:25:11 +0800
Subject: [PATCH 010/110] fix type errors of channel

---
 .../src/components/MkChannelFollowButton.vue  |  5 ++--
 .../client/src/components/MkChannelList.vue   |  9 ++++---
 .../src/components/MkChannelPreview.vue       |  3 ++-
 packages/client/src/components/MkPostForm.vue |  2 +-
 .../src/components/MkPostFormDialog.vue       |  2 +-
 packages/client/src/pages/channel.vue         | 12 +++++----
 packages/client/src/pages/channels.vue        | 11 ++++----
 packages/firefish-js/src/api.types.ts         | 27 ++++++++++++++-----
 packages/firefish-js/src/entities.ts          | 11 +++++++-
 9 files changed, 56 insertions(+), 26 deletions(-)

diff --git a/packages/client/src/components/MkChannelFollowButton.vue b/packages/client/src/components/MkChannelFollowButton.vue
index c1910bc595..98e239195c 100644
--- a/packages/client/src/components/MkChannelFollowButton.vue
+++ b/packages/client/src/components/MkChannelFollowButton.vue
@@ -27,10 +27,11 @@ import { ref } from "vue";
 import * as os from "@/os";
 import { i18n } from "@/i18n";
 import icon from "@/scripts/icon";
+import type { entities } from "firefish-js";
 
 const props = withDefaults(
 	defineProps<{
-		channel: Record<string, any>;
+		channel: entities.Channel;
 		full?: boolean;
 	}>(),
 	{
@@ -38,7 +39,7 @@ const props = withDefaults(
 	},
 );
 
-const isFollowing = ref<boolean>(props.channel.isFollowing);
+const isFollowing = ref<boolean>(props.channel.isFollowing ?? false);
 const wait = ref(false);
 
 async function onClick() {
diff --git a/packages/client/src/components/MkChannelList.vue b/packages/client/src/components/MkChannelList.vue
index 15f199c90f..ce2b7790d4 100644
--- a/packages/client/src/components/MkChannelList.vue
+++ b/packages/client/src/components/MkChannelList.vue
@@ -11,7 +11,7 @@
 			</div>
 		</template>
 
-		<template #default="{ items }">
+		<template #default="{ items }: { items: entities.Channel[] }">
 			<MkChannelPreview
 				v-for="item in items"
 				:key="item.id"
@@ -29,14 +29,15 @@ import MkPagination from "@/components/MkPagination.vue";
 import { i18n } from "@/i18n";
 import type { entities } from "firefish-js";
 
-const props = withDefaults(
+withDefaults(
 	defineProps<{
 		pagination: PagingOf<entities.Channel>;
 		noGap?: boolean;
-		extractor?: (item: any) => any;
+		// TODO: this function is not used and may can be removed
+		extractor?: (item: entities.Channel) => entities.Channel;
 	}>(),
 	{
-		extractor: (item) => item,
+		extractor: (item: entities.Channel) => item,
 	},
 );
 </script>
diff --git a/packages/client/src/components/MkChannelPreview.vue b/packages/client/src/components/MkChannelPreview.vue
index f824a1b2f5..8b2e12dc8b 100644
--- a/packages/client/src/components/MkChannelPreview.vue
+++ b/packages/client/src/components/MkChannelPreview.vue
@@ -54,9 +54,10 @@
 import { computed } from "vue";
 import { i18n } from "@/i18n";
 import icon from "@/scripts/icon";
+import type { entities } from "firefish-js";
 
 const props = defineProps<{
-	channel: Record<string, any>;
+	channel: entities.Channel;
 }>();
 
 const bannerStyle = computed(() => {
diff --git a/packages/client/src/components/MkPostForm.vue b/packages/client/src/components/MkPostForm.vue
index ba58ea652a..3254591bc3 100644
--- a/packages/client/src/components/MkPostForm.vue
+++ b/packages/client/src/components/MkPostForm.vue
@@ -354,7 +354,7 @@ const props = withDefaults(
 	defineProps<{
 		reply?: entities.Note;
 		renote?: entities.Note;
-		channel?: any; // TODO
+		channel?: entities.Channel;
 		mention?: entities.User;
 		specified?: entities.User;
 		initialText?: string;
diff --git a/packages/client/src/components/MkPostFormDialog.vue b/packages/client/src/components/MkPostFormDialog.vue
index 60637ff237..5ae9e520d4 100644
--- a/packages/client/src/components/MkPostFormDialog.vue
+++ b/packages/client/src/components/MkPostFormDialog.vue
@@ -28,7 +28,7 @@ import MkPostForm from "@/components/MkPostForm.vue";
 const props = defineProps<{
 	reply?: entities.Note;
 	renote?: entities.Note;
-	channel?: any; // TODO
+	channel?: entities.Channel;
 	mention?: entities.User;
 	specified?: entities.User;
 	initialText?: string;
diff --git a/packages/client/src/pages/channel.vue b/packages/client/src/pages/channel.vue
index 15e8b6e256..b8ec491ecb 100644
--- a/packages/client/src/pages/channel.vue
+++ b/packages/client/src/pages/channel.vue
@@ -33,7 +33,7 @@
 						:style="{
 							backgroundImage: channel.bannerUrl
 								? `url(${channel.bannerUrl})`
-								: null,
+								: undefined,
 						}"
 						class="banner"
 					>
@@ -88,8 +88,6 @@
 					class="_gap"
 					src="channel"
 					:channel="channelId"
-					@before="before"
-					@after="after"
 				/>
 			</div>
 		</MkSpacer>
@@ -107,6 +105,7 @@ import { me } from "@/me";
 import { i18n } from "@/i18n";
 import { definePageMetadata } from "@/scripts/page-metadata";
 import icon from "@/scripts/icon";
+import type { entities } from "firefish-js";
 
 const router = useRouter();
 
@@ -114,7 +113,11 @@ const props = defineProps<{
 	channelId: string;
 }>();
 
-const channel = ref(null);
+const channel = ref<entities.Channel>(
+	await os.api("channels/show", {
+		channelId: props.channelId,
+	}),
+);
 const showBanner = ref(true);
 
 watch(
@@ -124,7 +127,6 @@ watch(
 			channelId: props.channelId,
 		});
 	},
-	{ immediate: true },
 );
 
 function edit() {
diff --git a/packages/client/src/pages/channels.vue b/packages/client/src/pages/channels.vue
index ec92af54aa..5286f909d4 100644
--- a/packages/client/src/pages/channels.vue
+++ b/packages/client/src/pages/channels.vue
@@ -125,6 +125,7 @@ import { defaultStore } from "@/store";
 import icon from "@/scripts/icon";
 import "swiper/scss";
 import "swiper/scss/virtual";
+import type { Swiper as SwiperType } from "swiper/types";
 
 const router = useRouter();
 
@@ -216,18 +217,18 @@ definePageMetadata(
 	})),
 );
 
-let swiperRef = null;
+let swiperRef: SwiperType | null = null;
 
-function setSwiperRef(swiper) {
+function setSwiperRef(swiper: SwiperType) {
 	swiperRef = swiper;
 	syncSlide(tabs.indexOf(tab.value));
 }
 
 function onSlideChange() {
-	tab.value = tabs[swiperRef.activeIndex];
+	tab.value = tabs[swiperRef!.activeIndex];
 }
 
-function syncSlide(index) {
-	swiperRef.slideTo(index);
+function syncSlide(index: number) {
+	swiperRef!.slideTo(index);
 }
 </script>
diff --git a/packages/firefish-js/src/api.types.ts b/packages/firefish-js/src/api.types.ts
index 81e9a4a72c..9d1123e7ad 100644
--- a/packages/firefish-js/src/api.types.ts
+++ b/packages/firefish-js/src/api.types.ts
@@ -218,16 +218,31 @@ export type Endpoints = {
 	};
 
 	// channels
-	"channels/create": { req: TODO; res: TODO };
-	"channels/featured": { req: TODO; res: TODO };
+	"channels/create": {
+		req: {
+			name: string;
+			description?: string;
+			bannerId: DriveFile["id"] | null;
+		};
+		res: Channel;
+	};
+	"channels/featured": { req: TODO; res: Channel[] };
 	"channels/follow": { req: TODO; res: TODO };
-	"channels/followed": { req: TODO; res: TODO };
-	"channels/owned": { req: TODO; res: TODO };
+	"channels/followed": { req: TODO; res: Channel[] };
+	"channels/owned": { req: TODO; res: Channel[] };
 	"channels/pin-note": { req: TODO; res: TODO };
-	"channels/show": { req: TODO; res: TODO };
+	"channels/show": { req: TODO; res: Channel };
 	"channels/timeline": { req: TODO; res: Note[] };
 	"channels/unfollow": { req: TODO; res: TODO };
-	"channels/update": { req: TODO; res: TODO };
+	"channels/update": {
+		req: {
+			channelId: Channel["id"];
+			name: string;
+			description?: string;
+			bannerId: DriveFile["id"] | null;
+		};
+		res: Channel;
+	};
 
 	// charts
 	"charts/active-users": {
diff --git a/packages/firefish-js/src/entities.ts b/packages/firefish-js/src/entities.ts
index 086bf26193..74e97f2781 100644
--- a/packages/firefish-js/src/entities.ts
+++ b/packages/firefish-js/src/entities.ts
@@ -471,8 +471,17 @@ export type FollowRequest = {
 
 export type Channel = {
 	id: ID;
+	createdAt: DateString;
+	lastNotedAt: DateString | null;
 	name: string;
-	// TODO
+	description: string | null;
+	bannerId: DriveFile["id"];
+	bannerUrl: string | null;
+	notesCount: number;
+	usersCount: number;
+	isFollowing?: boolean;
+	userId: User["id"] | null;
+	hasUnreadNote?: boolean;
 };
 
 export type Following = {

From 69828437161751568ec291fc77cb07d9583c9585 Mon Sep 17 00:00:00 2001
From: Lhcfl <Lhcfl@outlook.com>
Date: Wed, 10 Apr 2024 22:25:45 +0800
Subject: [PATCH 011/110] fix: channel editor cannot remove channel banner

---
 .../src/models/repositories/channel.ts        |  1 +
 packages/backend/src/models/schema/channel.ts | 12 +++
 .../server/api/endpoints/channels/update.ts   |  2 +-
 packages/client/src/pages/channel-editor.vue  | 80 ++++++++++++-------
 4 files changed, 67 insertions(+), 28 deletions(-)

diff --git a/packages/backend/src/models/repositories/channel.ts b/packages/backend/src/models/repositories/channel.ts
index 857470f4ec..809129db6c 100644
--- a/packages/backend/src/models/repositories/channel.ts
+++ b/packages/backend/src/models/repositories/channel.ts
@@ -40,6 +40,7 @@ export const ChannelRepository = db.getRepository(Channel).extend({
 			name: channel.name,
 			description: channel.description,
 			userId: channel.userId,
+			bannerId: channel.bannerId,
 			bannerUrl: banner ? DriveFiles.getPublicUrl(banner, false) : null,
 			usersCount: channel.usersCount,
 			notesCount: channel.notesCount,
diff --git a/packages/backend/src/models/schema/channel.ts b/packages/backend/src/models/schema/channel.ts
index 67833cb0dd..d3ec222c8d 100644
--- a/packages/backend/src/models/schema/channel.ts
+++ b/packages/backend/src/models/schema/channel.ts
@@ -36,6 +36,13 @@ export const packedChannelSchema = {
 			nullable: true,
 			optional: false,
 		},
+		bannerId: {
+			type: "string",
+			optional: false,
+			nullable: true,
+			format: "id",
+			example: "xxxxxxxxxx",
+		},
 		notesCount: {
 			type: "number",
 			nullable: false,
@@ -57,5 +64,10 @@ export const packedChannelSchema = {
 			optional: false,
 			format: "id",
 		},
+		hasUnreadNote: {
+			type: "boolean",
+			optional: true,
+			nullable: false,
+		},
 	},
 } as const;
diff --git a/packages/backend/src/server/api/endpoints/channels/update.ts b/packages/backend/src/server/api/endpoints/channels/update.ts
index 0de7a837a1..fdd21da65f 100644
--- a/packages/backend/src/server/api/endpoints/channels/update.ts
+++ b/packages/backend/src/server/api/endpoints/channels/update.ts
@@ -83,7 +83,7 @@ export default define(meta, paramDef, async (ps, me) => {
 	await Channels.update(channel.id, {
 		...(ps.name !== undefined ? { name: ps.name } : {}),
 		...(ps.description !== undefined ? { description: ps.description } : {}),
-		...(banner ? { bannerId: banner.id } : {}),
+		...(banner ? { bannerId: banner.id } : { bannerId: null }),
 	});
 
 	return await Channels.pack(channel.id, me);
diff --git a/packages/client/src/pages/channel-editor.vue b/packages/client/src/pages/channel-editor.vue
index 60bcfe990e..3791a98d05 100644
--- a/packages/client/src/pages/channel-editor.vue
+++ b/packages/client/src/pages/channel-editor.vue
@@ -50,6 +50,7 @@ import { useRouter } from "@/router";
 import { definePageMetadata } from "@/scripts/page-metadata";
 import { i18n } from "@/i18n";
 import icon from "@/scripts/icon";
+import type { entities } from "firefish-js";
 
 const router = useRouter();
 
@@ -57,26 +58,24 @@ const props = defineProps<{
 	channelId?: string;
 }>();
 
-const channel = ref(null);
-const name = ref(null);
-const description = ref(null);
+const channel = ref<entities.Channel | null>(null);
+const name = ref<string>("");
+const description = ref<string>("");
 const bannerUrl = ref<string | null>(null);
 const bannerId = ref<string | null>(null);
 
-watch(
-	() => bannerId.value,
-	async () => {
-		if (bannerId.value == null) {
-			bannerUrl.value = null;
-		} else {
-			bannerUrl.value = (
-				await os.api("drive/files/show", {
-					fileId: bannerId.value,
-				})
-			).url;
-		}
-	},
-);
+let bannerUrlUpdated = false;
+
+/**
+ * Set banner url and id when we already know the url
+ * Prevent redundant network requests from being sent
+ */
+function setBanner(opt: { bannerId: string | null; bannerUrl: string | null }) {
+	bannerUrlUpdated = true;
+	bannerUrl.value = opt.bannerUrl;
+	bannerId.value = opt.bannerId;
+	bannerUrlUpdated = false;
+}
 
 async function fetchChannel() {
 	if (props.channelId == null) return;
@@ -86,23 +85,44 @@ async function fetchChannel() {
 	});
 
 	name.value = channel.value.name;
-	description.value = channel.value.description;
-	bannerId.value = channel.value.bannerId;
-	bannerUrl.value = channel.value.bannerUrl;
+	description.value = channel.value.description ?? "";
+	setBanner(channel.value);
 }
 
-fetchChannel();
+await fetchChannel();
+
+watch(bannerId, async () => {
+	if (bannerUrlUpdated) {
+		bannerUrlUpdated = false;
+		return;
+	}
+	if (bannerId.value == null) {
+		bannerUrl.value = null;
+	} else {
+		bannerUrl.value = (
+			await os.api("drive/files/show", {
+				fileId: bannerId.value,
+			})
+		).url;
+	}
+});
 
 function save() {
-	const params = {
+	const params: {
+		name: string;
+		description: string;
+		bannerId: string | null;
+	} = {
 		name: name.value,
 		description: description.value,
 		bannerId: bannerId.value,
 	};
 
 	if (props.channelId) {
-		params.channelId = props.channelId;
-		os.api("channels/update", params).then(() => {
+		os.api("channels/update", {
+			...params,
+			channelId: props.channelId,
+		}).then(() => {
 			os.success();
 		});
 	} else {
@@ -113,14 +133,20 @@ function save() {
 	}
 }
 
-function setBannerImage(evt) {
+function setBannerImage(evt: MouseEvent) {
 	selectFile(evt.currentTarget ?? evt.target, null).then((file) => {
-		bannerId.value = file.id;
+		setBanner({
+			bannerId: file.id,
+			bannerUrl: file.url,
+		});
 	});
 }
 
 function removeBannerImage() {
-	bannerId.value = null;
+	setBanner({
+		bannerId: null,
+		bannerUrl: null,
+	});
 }
 
 const headerActions = computed(() => []);

From 0b7ab1b90dc97e94007a5c17251c921fc9282019 Mon Sep 17 00:00:00 2001
From: Lhcfl <Lhcfl@outlook.com>
Date: Wed, 10 Apr 2024 22:45:01 +0800
Subject: [PATCH 012/110] fix type errors of MkChatPreview

---
 .../client/src/components/MkChatPreview.vue   | 24 +++++++++++--------
 packages/firefish-js/src/entities.ts          |  4 +++-
 2 files changed, 17 insertions(+), 11 deletions(-)

diff --git a/packages/client/src/components/MkChatPreview.vue b/packages/client/src/components/MkChatPreview.vue
index 1b330b7b2d..6976a3f18a 100644
--- a/packages/client/src/components/MkChatPreview.vue
+++ b/packages/client/src/components/MkChatPreview.vue
@@ -4,14 +4,14 @@
 		:class="{
 			isMe: isMe(message),
 			isRead: message.groupId
-				? message.reads.includes(me?.id)
+				? message.reads.includes(me!.id)
 				: message.isRead,
 		}"
 		:to="
 			message.groupId
 				? `/my/messaging/group/${message.groupId}`
 				: `/my/messaging/${acct.toString(
-						isMe(message) ? message.recipient : message.user,
+						isMe(message) ? message.recipient! : message.user,
 					)}`
 		"
 	>
@@ -22,27 +22,27 @@
 					message.groupId
 						? message.user
 						: isMe(message)
-							? message.recipient
+							? message.recipient!
 							: message.user
 				"
 				:show-indicator="true"
 				disable-link
 			/>
 			<header v-if="message.groupId">
-				<span class="name">{{ message.group.name }}</span>
+				<span class="name">{{ message.group!.name }}</span>
 				<MkTime :time="message.createdAt" class="time" />
 			</header>
 			<header v-else>
 				<span class="name"
 					><MkUserName
 						:user="
-							isMe(message) ? message.recipient : message.user
+							isMe(message) ? message.recipient! : message.user
 						"
 				/></span>
 				<span class="username"
 					>@{{
 						acct.toString(
-							isMe(message) ? message.recipient : message.user,
+							isMe(message) ? message.recipient! : message.user,
 						)
 					}}</span
 				>
@@ -65,16 +65,20 @@
 </template>
 
 <script lang="ts" setup>
-import { acct } from "firefish-js";
+import { acct, type entities } from "firefish-js";
 import { i18n } from "@/i18n";
 import { me } from "@/me";
 
+if (me == null) {
+	throw "No me";
+}
+
 defineProps<{
-	message: Record<string, any>;
+	message: entities.MessagingMessage;
 }>();
 
-function isMe(message): boolean {
-	return message.userId === me?.id;
+function isMe(message: entities.MessagingMessage): boolean {
+	return message.userId === me!.id;
 }
 </script>
 
diff --git a/packages/firefish-js/src/entities.ts b/packages/firefish-js/src/entities.ts
index 74e97f2781..16cef32827 100644
--- a/packages/firefish-js/src/entities.ts
+++ b/packages/firefish-js/src/entities.ts
@@ -76,7 +76,9 @@ export type UserDetailed = UserLite & {
 	url: string | null;
 };
 
-export type UserGroup = TODO;
+export type UserGroup = {
+	id: ID;
+} & Record<string, TODO>;
 
 export type UserList = {
 	id: ID;

From 124b2244d62b259dc20f103bd2004cfd94ada76d Mon Sep 17 00:00:00 2001
From: Lhcfl <Lhcfl@outlook.com>
Date: Wed, 10 Apr 2024 22:50:30 +0800
Subject: [PATCH 013/110] fix type errors of MkCode & MkModalWindow

---
 packages/client/src/components/MkCode.core.vue   | 1 +
 packages/client/src/components/MkModalWindow.vue | 2 +-
 2 files changed, 2 insertions(+), 1 deletion(-)

diff --git a/packages/client/src/components/MkCode.core.vue b/packages/client/src/components/MkCode.core.vue
index 11720b4f62..7e3cdac562 100644
--- a/packages/client/src/components/MkCode.core.vue
+++ b/packages/client/src/components/MkCode.core.vue
@@ -29,6 +29,7 @@ if (props.lang != null && !(props.lang in Prism.languages)) {
 	const { lang } = props;
 	loadLanguage(props.lang).then(
 		// onLoaded
+		// biome-ignore lint/suspicious/noAssignInExpressions: assign intentionally
 		() => (prismLang.value = lang),
 		// onError
 		() => {},
diff --git a/packages/client/src/components/MkModalWindow.vue b/packages/client/src/components/MkModalWindow.vue
index b7ba30dd17..3d04ce00de 100644
--- a/packages/client/src/components/MkModalWindow.vue
+++ b/packages/client/src/components/MkModalWindow.vue
@@ -97,7 +97,7 @@ const modal = shallowRef<InstanceType<typeof MkModal>>();
 const rootEl = shallowRef<HTMLElement>();
 const headerEl = shallowRef<HTMLElement>();
 
-const close = (ev) => {
+const close = (ev?) => {
 	modal.value?.close(ev);
 };
 

From 909125e519829b59e06c2a2f4213a9d521877f9f Mon Sep 17 00:00:00 2001
From: Lhcfl <Lhcfl@outlook.com>
Date: Wed, 10 Apr 2024 23:35:14 +0800
Subject: [PATCH 014/110] refactor: rewrite MkContainer using composition api

---
 .../client/src/components/MkContainer.vue     | 201 ++++++++----------
 1 file changed, 92 insertions(+), 109 deletions(-)

diff --git a/packages/client/src/components/MkContainer.vue b/packages/client/src/components/MkContainer.vue
index 404f984415..6d91b39fca 100644
--- a/packages/client/src/components/MkContainer.vue
+++ b/packages/client/src/components/MkContainer.vue
@@ -9,6 +9,7 @@
 			scrollable,
 			closed: !showBody,
 		}"
+		ref="el"
 	>
 		<header v-if="showHeader" ref="header">
 			<div class="title"><slot name="header"></slot></div>
@@ -59,123 +60,105 @@
 	</div>
 </template>
 
-<script lang="ts">
-import { defineComponent } from "vue";
+<script lang="ts" setup>
+import { onMounted, ref, watch } from "vue";
 import { i18n } from "@/i18n";
 import { defaultStore } from "@/store";
 import icon from "@/scripts/icon";
 
-export default defineComponent({
-	props: {
-		showHeader: {
-			type: Boolean,
-			required: false,
-			default: true,
-		},
-		thin: {
-			type: Boolean,
-			required: false,
-			default: false,
-		},
-		naked: {
-			type: Boolean,
-			required: false,
-			default: false,
-		},
-		foldable: {
-			type: Boolean,
-			required: false,
-			default: false,
-		},
-		expanded: {
-			type: Boolean,
-			required: false,
-			default: true,
-		},
-		scrollable: {
-			type: Boolean,
-			required: false,
-			default: false,
-		},
-		maxHeight: {
-			type: Number,
-			required: false,
-			default: null,
-		},
-	},
-	data() {
-		return {
-			showBody: this.expanded,
-			omitted: null,
-			ignoreOmit: false,
-			i18n,
-			icon,
-			defaultStore,
-		};
-	},
-	mounted() {
-		this.$watch(
-			"showBody",
-			(showBody) => {
-				const headerHeight = this.showHeader
-					? this.$refs.header.offsetHeight
-					: 0;
-				this.$el.style.minHeight = `${headerHeight}px`;
-				if (showBody) {
-					this.$el.style.flexBasis = "auto";
-				} else {
-					this.$el.style.flexBasis = `${headerHeight}px`;
-				}
-			},
-			{
-				immediate: true,
-			},
-		);
+const props = withDefaults(
+	defineProps<{
+		showHeader?: boolean,
+		thin?: boolean,
+		naked?: boolean,
+		foldable?: boolean,
+		expanded?: boolean,
+		scrollable?: boolean,
+		maxHeight?: number | null,
+	}>(),
+	{
+		showHeader: true,
+		thin: false,
+		naked: false,
+		foldable: false,
+		expanded: true,
+		scrollable: false,
+		maxHeight: null,
+	}
+);
 
-		this.$el.style.setProperty("--maxHeight", this.maxHeight + "px");
+const showBody = ref(props.expanded);
+const omitted = ref<boolean | null>(null);
+const ignoreOmit = ref(false);
+const el = ref<HTMLElement | null>(null);
+const header = ref<HTMLElement | null>(null);
+const content = ref<HTMLElement | null>(null);
 
-		const calcOmit = () => {
-			if (
-				this.omitted ||
-				this.ignoreOmit ||
-				this.maxHeight == null ||
-				this.$refs.content == null
-			)
-				return;
-			const height = this.$refs.content.offsetHeight;
-			this.omitted = height > this.maxHeight;
-		};
+// FIXME: This function is not used, why?
+function toggleContent(show: boolean) {
+	if (!props.foldable) return;
+	showBody.value = show;
+}
 
+function enter(el) {
+	const elementHeight = el.getBoundingClientRect().height;
+	el.style.height = 0;
+	el.offsetHeight; // reflow
+	el.style.height = `${elementHeight}px`;
+}
+function afterEnter(el) {
+	el.style.height = null;
+}
+function leave(el) {
+	const elementHeight = el.getBoundingClientRect().height;
+	el.style.height = `${elementHeight}px`;
+	el.offsetHeight; // reflow
+	el.style.height = 0;
+}
+function afterLeave(el) {
+	el.style.height = null;
+}
+
+onMounted(() => {
+	watch(
+		showBody,
+		(showBody) => {
+			const headerHeight = props.showHeader
+				? header.value!.offsetHeight
+				: 0;
+			el.value!.style.minHeight = `${headerHeight}px`;
+			if (showBody) {
+				el.value!.style.flexBasis = "auto";
+			} else {
+				el.value!.style.flexBasis = `${headerHeight}px`;
+			}
+		},
+		{
+			immediate: true,
+		}
+	);
+
+	if (props.maxHeight != null) {
+		el.value!.style.setProperty("--maxHeight", `${props.maxHeight}px`);
+	}
+
+	const calcOmit = () => {
+		if (
+			omitted.value ||
+			ignoreOmit.value ||
+			props.maxHeight == null ||
+			content.value == null
+		)
+			return;
+		const height = content.value.offsetHeight;
+		omitted.value = height > props.maxHeight;
+	};
+
+	calcOmit();
+
+	new ResizeObserver((_entries, _observer) => {
 		calcOmit();
-		new ResizeObserver((entries, observer) => {
-			calcOmit();
-		}).observe(this.$refs.content);
-	},
-	methods: {
-		toggleContent(show: boolean) {
-			if (!this.foldable) return;
-			this.showBody = show;
-		},
-
-		enter(el) {
-			const elementHeight = el.getBoundingClientRect().height;
-			el.style.height = 0;
-			el.offsetHeight; // reflow
-			el.style.height = elementHeight + "px";
-		},
-		afterEnter(el) {
-			el.style.height = null;
-		},
-		leave(el) {
-			const elementHeight = el.getBoundingClientRect().height;
-			el.style.height = elementHeight + "px";
-			el.offsetHeight; // reflow
-			el.style.height = 0;
-		},
-		afterLeave(el) {
-			el.style.height = null;
-		},
-	},
+	}).observe(content.value!);
 });
 </script>
 

From 43aeec32ce1c8d3c49793cdf8bcffce6663a0d40 Mon Sep 17 00:00:00 2001
From: Lhcfl <Lhcfl@outlook.com>
Date: Wed, 10 Apr 2024 23:36:42 +0800
Subject: [PATCH 015/110] fix types error of MkContextMenu

---
 packages/client/src/components/MkContextMenu.vue | 10 +++++-----
 1 file changed, 5 insertions(+), 5 deletions(-)

diff --git a/packages/client/src/components/MkContextMenu.vue b/packages/client/src/components/MkContextMenu.vue
index 319f7fd0fe..86ca3b3f3e 100644
--- a/packages/client/src/components/MkContextMenu.vue
+++ b/packages/client/src/components/MkContextMenu.vue
@@ -28,7 +28,7 @@ const emit = defineEmits<{
 	(ev: "closed"): void;
 }>();
 
-const rootEl = ref<HTMLDivElement>();
+const rootEl = ref<HTMLDivElement | null>(null);
 
 const zIndex = ref<number>(os.claimZIndex("high"));
 
@@ -36,8 +36,8 @@ onMounted(() => {
 	let left = props.ev.pageX + 1; // 間違って右ダブルクリックした場合に意図せずアイテムがクリックされるのを防ぐため + 1
 	let top = props.ev.pageY + 1; // 間違って右ダブルクリックした場合に意図せずアイテムがクリックされるのを防ぐため + 1
 
-	const width = rootEl.value.offsetWidth;
-	const height = rootEl.value.offsetHeight;
+	const width = rootEl.value!.offsetWidth;
+	const height = rootEl.value!.offsetHeight;
 
 	if (left + width - window.scrollX > window.innerWidth) {
 		left = window.innerWidth - width + window.scrollX;
@@ -55,8 +55,8 @@ onMounted(() => {
 		left = 0;
 	}
 
-	rootEl.value.style.top = `${top}px`;
-	rootEl.value.style.left = `${left}px`;
+	rootEl.value!.style.top = `${top}px`;
+	rootEl.value!.style.left = `${left}px`;
 
 	document.body.addEventListener("mousedown", onMousedown);
 });

From 18ba024cbb06f24406ad207be6b933f96bf87980 Mon Sep 17 00:00:00 2001
From: Lhcfl <Lhcfl@outlook.com>
Date: Thu, 11 Apr 2024 00:00:54 +0800
Subject: [PATCH 016/110] chore: format

---
 .../client/src/components/MkContainer.vue     | 22 +++++++++----------
 1 file changed, 10 insertions(+), 12 deletions(-)

diff --git a/packages/client/src/components/MkContainer.vue b/packages/client/src/components/MkContainer.vue
index 6d91b39fca..dfc5ec26ba 100644
--- a/packages/client/src/components/MkContainer.vue
+++ b/packages/client/src/components/MkContainer.vue
@@ -68,13 +68,13 @@ import icon from "@/scripts/icon";
 
 const props = withDefaults(
 	defineProps<{
-		showHeader?: boolean,
-		thin?: boolean,
-		naked?: boolean,
-		foldable?: boolean,
-		expanded?: boolean,
-		scrollable?: boolean,
-		maxHeight?: number | null,
+		showHeader?: boolean;
+		thin?: boolean;
+		naked?: boolean;
+		foldable?: boolean;
+		expanded?: boolean;
+		scrollable?: boolean;
+		maxHeight?: number | null;
 	}>(),
 	{
 		showHeader: true,
@@ -84,7 +84,7 @@ const props = withDefaults(
 		expanded: true,
 		scrollable: false,
 		maxHeight: null,
-	}
+	},
 );
 
 const showBody = ref(props.expanded);
@@ -123,9 +123,7 @@ onMounted(() => {
 	watch(
 		showBody,
 		(showBody) => {
-			const headerHeight = props.showHeader
-				? header.value!.offsetHeight
-				: 0;
+			const headerHeight = props.showHeader ? header.value!.offsetHeight : 0;
 			el.value!.style.minHeight = `${headerHeight}px`;
 			if (showBody) {
 				el.value!.style.flexBasis = "auto";
@@ -135,7 +133,7 @@ onMounted(() => {
 		},
 		{
 			immediate: true,
-		}
+		},
 	);
 
 	if (props.maxHeight != null) {

From f275fc9cdf0e1d80c4528a6cf13f525cd3930b22 Mon Sep 17 00:00:00 2001
From: Lhcfl <Lhcfl@outlook.com>
Date: Thu, 11 Apr 2024 00:14:36 +0800
Subject: [PATCH 017/110] refactor: fix type errors of MkCropperDialog

---
 .../client/src/components/MkCropperDialog.vue | 56 +++++++++++--------
 .../client/src/components/MkModalWindow.vue   |  5 +-
 packages/client/src/os.ts                     |  8 +--
 3 files changed, 40 insertions(+), 29 deletions(-)

diff --git a/packages/client/src/components/MkCropperDialog.vue b/packages/client/src/components/MkCropperDialog.vue
index 16b42c2f2a..955481bdad 100644
--- a/packages/client/src/components/MkCropperDialog.vue
+++ b/packages/client/src/components/MkCropperDialog.vue
@@ -68,40 +68,48 @@ let cropper: Cropper | null = null;
 const loading = ref(true);
 
 const ok = async () => {
-	const promise = new Promise<entities.DriveFile>(async (res) => {
+	async function UploadCroppedImg(): Promise<entities.DriveFile> {
 		const croppedCanvas = await cropper?.getCropperSelection()?.$toCanvas();
-		croppedCanvas.toBlob((blob) => {
-			const formData = new FormData();
-			formData.append("file", blob);
-			if (defaultStore.state.uploadFolder) {
-				formData.append("folderId", defaultStore.state.uploadFolder);
-			}
 
-			fetch(apiUrl + "/drive/files/create", {
-				method: "POST",
-				body: formData,
-				headers: {
-					authorization: `Bearer ${me.token}`,
-				},
-			})
-				.then((response) => response.json())
-				.then((f) => {
-					res(f);
-				});
+		const blob = await new Promise<Blob | null>((resolve) =>
+			croppedCanvas!.toBlob((blob) => resolve(blob)),
+		);
+
+		// MDN says `null` may be passed if the image cannot be created for any reason.
+		// But I don't think this is reachable for normal case.
+		if (blob == null) {
+			throw "Cropping image failed.";
+		}
+
+		const formData = new FormData();
+		formData.append("file", blob);
+		if (defaultStore.state.uploadFolder) {
+			formData.append("folderId", defaultStore.state.uploadFolder);
+		}
+
+		const response = await fetch(`${apiUrl}/drive/files/create`, {
+			method: "POST",
+			body: formData,
+			headers: {
+				authorization: `Bearer ${me!.token}`,
+			},
 		});
-	});
+		return await response.json();
+	}
+
+	const promise = UploadCroppedImg();
 
 	os.promiseDialog(promise);
 
 	const f = await promise;
 
 	emit("ok", f);
-	dialogEl.value.close();
+	dialogEl.value!.close();
 };
 
 const cancel = () => {
 	emit("cancel");
-	dialogEl.value.close();
+	dialogEl.value!.close();
 };
 
 const onImageLoad = () => {
@@ -114,7 +122,7 @@ const onImageLoad = () => {
 };
 
 onMounted(() => {
-	cropper = new Cropper(imgEl.value, {});
+	cropper = new Cropper(imgEl.value!, {});
 
 	const computedStyle = getComputedStyle(document.documentElement);
 
@@ -127,13 +135,13 @@ onMounted(() => {
 	selection.outlined = true;
 
 	window.setTimeout(() => {
-		cropper.getCropperImage()!.$center("contain");
+		cropper!.getCropperImage()!.$center("contain");
 		selection.$center();
 	}, 100);
 
 	// モーダルオープンアニメーションが終わったあとで再度調整
 	window.setTimeout(() => {
-		cropper.getCropperImage()!.$center("contain");
+		cropper!.getCropperImage()!.$center("contain");
 		selection.$center();
 	}, 500);
 });
diff --git a/packages/client/src/components/MkModalWindow.vue b/packages/client/src/components/MkModalWindow.vue
index 3d04ce00de..c9d126ee90 100644
--- a/packages/client/src/components/MkModalWindow.vue
+++ b/packages/client/src/components/MkModalWindow.vue
@@ -54,7 +54,10 @@
 					</button>
 				</div>
 				<div class="body">
-					<slot></slot>
+					<slot
+						:width="width"
+						:height="height"
+					></slot>
 				</div>
 			</div>
 		</FocusTrap>
diff --git a/packages/client/src/os.ts b/packages/client/src/os.ts
index b8fac741ea..328b961b21 100644
--- a/packages/client/src/os.ts
+++ b/packages/client/src/os.ts
@@ -125,12 +125,12 @@ export const apiWithDialog = ((
 	return promise;
 }) as typeof api;
 
-export function promiseDialog<T extends Promise<any>>(
-	promise: T,
-	onSuccess?: ((res: any) => void) | null,
+export function promiseDialog<T>(
+	promise: Promise<T>,
+	onSuccess?: ((res: T) => void) | null,
 	onFailure?: ((err: Error) => void) | null,
 	text?: string,
-): T {
+): Promise<T> {
 	const showing = ref(true);
 	const success = ref(false);
 

From 3ddd68097abcc2416d1e42cfa2c11ec497789ac4 Mon Sep 17 00:00:00 2001
From: Lhcfl <Lhcfl@outlook.com>
Date: Thu, 11 Apr 2024 00:48:35 +0800
Subject: [PATCH 018/110] fix type errors

---
 packages/client/src/components/MkCwButton.vue            | 2 +-
 packages/client/src/components/MkDigitalClock.vue        | 2 +-
 packages/client/src/components/MkDonation.vue            | 7 ++++---
 packages/client/src/components/MkDrive.file.vue          | 3 ++-
 packages/client/src/components/MkEmojiPicker.section.vue | 2 +-
 packages/firefish-js/src/entities.ts                     | 1 +
 6 files changed, 10 insertions(+), 7 deletions(-)

diff --git a/packages/client/src/components/MkCwButton.vue b/packages/client/src/components/MkCwButton.vue
index e7a2d3d77a..ccdea3ec90 100644
--- a/packages/client/src/components/MkCwButton.vue
+++ b/packages/client/src/components/MkCwButton.vue
@@ -48,7 +48,7 @@ const toggle = () => {
 };
 
 function focus() {
-	el.value.focus();
+	el.value?.focus();
 }
 
 defineExpose({
diff --git a/packages/client/src/components/MkDigitalClock.vue b/packages/client/src/components/MkDigitalClock.vue
index b42ecf19eb..42eabdf749 100644
--- a/packages/client/src/components/MkDigitalClock.vue
+++ b/packages/client/src/components/MkDigitalClock.vue
@@ -26,7 +26,7 @@ const props = withDefaults(
 	},
 );
 
-let intervalId;
+let intervalId: number;
 const hh = ref("");
 const mm = ref("");
 const ss = ref("");
diff --git a/packages/client/src/components/MkDonation.vue b/packages/client/src/components/MkDonation.vue
index 1c13754c13..ad9df629f4 100644
--- a/packages/client/src/components/MkDonation.vue
+++ b/packages/client/src/components/MkDonation.vue
@@ -29,7 +29,7 @@
 					<MkButton
 						v-if="instance.donationLink"
 						gradate
-						@click="openExternal(instance.donationLink)"
+						@click="openExternal(instance.donationLink!)"
 						>{{
 							i18n.t("_aboutFirefish.donateHost", {
 								host: hostname,
@@ -73,7 +73,8 @@ const emit = defineEmits<{
 	(ev: "closed"): void;
 }>();
 
-const hostname = instance.name?.length < 38 ? instance.name : host;
+const hostname =
+	instance.name?.length && instance.name?.length < 38 ? instance.name : host;
 
 const zIndex = os.claimZIndex("low");
 
@@ -97,7 +98,7 @@ function neverShow() {
 	close();
 }
 
-function openExternal(link) {
+function openExternal(link: string) {
 	window.open(link, "_blank");
 }
 </script>
diff --git a/packages/client/src/components/MkDrive.file.vue b/packages/client/src/components/MkDrive.file.vue
index 5e8a50eec2..9d09da663e 100644
--- a/packages/client/src/components/MkDrive.file.vue
+++ b/packages/client/src/components/MkDrive.file.vue
@@ -47,6 +47,7 @@ import * as os from "@/os";
 import { i18n } from "@/i18n";
 import { me } from "@/me";
 import icon from "@/scripts/icon";
+import type { MenuItem } from "@/types/menu";
 
 const props = withDefaults(
 	defineProps<{
@@ -72,7 +73,7 @@ const title = computed(
 	() => `${props.file.name}\n${props.file.type} ${bytes(props.file.size)}`,
 );
 
-function getMenu() {
+function getMenu(): MenuItem[] {
 	return [
 		{
 			text: i18n.ts.rename,
diff --git a/packages/client/src/components/MkEmojiPicker.section.vue b/packages/client/src/components/MkEmojiPicker.section.vue
index b91195fe65..9fcd5d363d 100644
--- a/packages/client/src/components/MkEmojiPicker.section.vue
+++ b/packages/client/src/components/MkEmojiPicker.section.vue
@@ -14,7 +14,7 @@
 					class="_button"
 					@click.stop="
 						applyUnicodeSkinTone(
-							props.skinTones.indexOf(skinTone) + 1,
+							props.skinTones!.indexOf(skinTone) + 1,
 						)
 					"
 				>
diff --git a/packages/firefish-js/src/entities.ts b/packages/firefish-js/src/entities.ts
index 16cef32827..387f4eb608 100644
--- a/packages/firefish-js/src/entities.ts
+++ b/packages/firefish-js/src/entities.ts
@@ -329,6 +329,7 @@ export type LiteInstanceMetadata = {
 	name: string | null;
 	uri: string;
 	description: string | null;
+	donationLink?: string;
 	tosUrl: string | null;
 	disableRegistration: boolean;
 	disableLocalTimeline: boolean;

From 783f5481bb762043aed1cec90355000e4ba6bc4a Mon Sep 17 00:00:00 2001
From: Lhcfl <Lhcfl@outlook.com>
Date: Thu, 11 Apr 2024 16:58:58 +0800
Subject: [PATCH 019/110] remove unnecessary assertion

---
 packages/client/src/components/MkChatPreview.vue | 4 ----
 1 file changed, 4 deletions(-)

diff --git a/packages/client/src/components/MkChatPreview.vue b/packages/client/src/components/MkChatPreview.vue
index 6976a3f18a..62235b9a22 100644
--- a/packages/client/src/components/MkChatPreview.vue
+++ b/packages/client/src/components/MkChatPreview.vue
@@ -69,10 +69,6 @@ import { acct, type entities } from "firefish-js";
 import { i18n } from "@/i18n";
 import { me } from "@/me";
 
-if (me == null) {
-	throw "No me";
-}
-
 defineProps<{
 	message: entities.MessagingMessage;
 }>();

From 7a4e6334f16c73edff97f7cbd225e26c85bbd881 Mon Sep 17 00:00:00 2001
From: Lhcfl <Lhcfl@outlook.com>
Date: Thu, 11 Apr 2024 17:01:37 +0800
Subject: [PATCH 020/110] fix type error

---
 packages/firefish-js/src/entities.ts | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/packages/firefish-js/src/entities.ts b/packages/firefish-js/src/entities.ts
index 387f4eb608..cafbdc7864 100644
--- a/packages/firefish-js/src/entities.ts
+++ b/packages/firefish-js/src/entities.ts
@@ -425,8 +425,8 @@ export type Announcement = {
 	title: string;
 	imageUrl: string | null;
 	isRead?: boolean;
-	isGoodNews?: boolean;
-	showPopUp?: boolean;
+	isGoodNews: boolean;
+	showPopUp: boolean;
 };
 
 export type Antenna = {

From 3716e7f74cde6e1aa2753074eecfc37715214277 Mon Sep 17 00:00:00 2001
From: Lhcfl <Lhcfl@outlook.com>
Date: Thu, 11 Apr 2024 17:11:48 +0800
Subject: [PATCH 021/110] fix: expose toggleContent for it was a method

---
 packages/client/src/components/MkContainer.vue | 9 ++++++++-
 1 file changed, 8 insertions(+), 1 deletion(-)

diff --git a/packages/client/src/components/MkContainer.vue b/packages/client/src/components/MkContainer.vue
index dfc5ec26ba..e933b02dbf 100644
--- a/packages/client/src/components/MkContainer.vue
+++ b/packages/client/src/components/MkContainer.vue
@@ -94,7 +94,6 @@ const el = ref<HTMLElement | null>(null);
 const header = ref<HTMLElement | null>(null);
 const content = ref<HTMLElement | null>(null);
 
-// FIXME: This function is not used, why?
 function toggleContent(show: boolean) {
 	if (!props.foldable) return;
 	showBody.value = show;
@@ -158,6 +157,14 @@ onMounted(() => {
 		calcOmit();
 	}).observe(content.value!);
 });
+
+defineExpose({
+	toggleContent,
+	enter,
+	afterEnter,
+	leave,
+	afterLeave,
+});
 </script>
 
 <style lang="scss" scoped>

From db37eb4ad123cfe1352a27d640b8231750bfb296 Mon Sep 17 00:00:00 2001
From: Lhcfl <Lhcfl@outlook.com>
Date: Thu, 11 Apr 2024 17:29:07 +0800
Subject: [PATCH 022/110] refactor: rewrite MkFolder

---
 packages/client/src/components/MkFolder.vue | 120 ++++++++++----------
 1 file changed, 62 insertions(+), 58 deletions(-)

diff --git a/packages/client/src/components/MkFolder.vue b/packages/client/src/components/MkFolder.vue
index 8f545406d1..1e69996054 100644
--- a/packages/client/src/components/MkFolder.vue
+++ b/packages/client/src/components/MkFolder.vue
@@ -31,72 +31,76 @@
 	</section>
 </template>
 
-<script lang="ts">
-import { defineComponent } from "vue";
+<script lang="ts" setup>
+import { ref, watch } from "vue";
 import { getUniqueId } from "@/os";
 import { defaultStore } from "@/store";
 // import icon from "@/scripts/icon";
 
 const localStoragePrefix = "ui:folder:";
 
-export default defineComponent({
-	props: {
-		expanded: {
-			type: Boolean,
-			required: false,
-			default: true,
-		},
-		persistKey: {
-			type: String,
-			required: false,
-			default: null,
-		},
+const props = withDefaults(
+	defineProps<{
+		expanded?: boolean;
+		persistKey?: string | null;
+	}>(),
+	{
+		expanded: true,
+		persistKey: null,
 	},
-	data() {
-		return {
-			bodyId: getUniqueId(),
-			showBody:
-				this.persistKey &&
-				localStorage.getItem(localStoragePrefix + this.persistKey)
-					? localStorage.getItem(localStoragePrefix + this.persistKey) === "t"
-					: this.expanded,
-			animation: defaultStore.state.animation,
-		};
-	},
-	watch: {
-		showBody() {
-			if (this.persistKey) {
-				localStorage.setItem(
-					localStoragePrefix + this.persistKey,
-					this.showBody ? "t" : "f",
-				);
-			}
-		},
-	},
-	methods: {
-		toggleContent(show: boolean) {
-			this.showBody = show;
-		},
+);
 
-		enter(el) {
-			const elementHeight = el.getBoundingClientRect().height;
-			el.style.height = 0;
-			el.offsetHeight; // reflow
-			el.style.height = elementHeight + "px";
-		},
-		afterEnter(el) {
-			el.style.height = null;
-		},
-		leave(el) {
-			const elementHeight = el.getBoundingClientRect().height;
-			el.style.height = elementHeight + "px";
-			el.offsetHeight; // reflow
-			el.style.height = 0;
-		},
-		afterLeave(el) {
-			el.style.height = null;
-		},
-	},
+const bodyId = ref(getUniqueId());
+
+const showBody = ref(
+	props.persistKey &&
+		localStorage.getItem(localStoragePrefix + props.persistKey)
+		? localStorage.getItem(localStoragePrefix + props.persistKey) === "t"
+		: props.expanded,
+);
+
+const animation = defaultStore.state.animation;
+
+watch(showBody, () => {
+	if (props.persistKey) {
+		localStorage.setItem(
+			localStoragePrefix + props.persistKey,
+			showBody.value ? "t" : "f",
+		);
+	}
+});
+
+function toggleContent(show: boolean) {
+	showBody.value = show;
+}
+
+function enter(el) {
+	const elementHeight = el.getBoundingClientRect().height;
+	el.style.height = 0;
+	el.offsetHeight; // reflow
+	// biome-ignore lint/style/useTemplate: <explanation>
+	el.style.height = elementHeight + "px";
+}
+function afterEnter(el) {
+	el.style.height = null;
+}
+function leave(el) {
+	const elementHeight = el.getBoundingClientRect().height;
+	// biome-ignore lint/style/useTemplate: <explanation>
+	el.style.height = elementHeight + "px";
+	el.offsetHeight; // reflow
+	el.style.height = 0;
+}
+function afterLeave(el) {
+	el.style.height = null;
+}
+
+defineExpose({
+	toggleContent,
+	enter,
+	afterEnter,
+	leave,
+	afterLeave,
 });
 </script>
 

From c4a093209f9b30a9137e0f0b60db3cdeb6c5a8d6 Mon Sep 17 00:00:00 2001
From: Lhcfl <Lhcfl@outlook.com>
Date: Thu, 11 Apr 2024 18:00:37 +0800
Subject: [PATCH 023/110] fix type of MkEmojiPicker

---
 .../client/src/components/MkEmojiPicker.vue   | 43 ++++++++++++++-----
 1 file changed, 33 insertions(+), 10 deletions(-)

diff --git a/packages/client/src/components/MkEmojiPicker.vue b/packages/client/src/components/MkEmojiPicker.vue
index ac2e1a8111..4cb9bfad48 100644
--- a/packages/client/src/components/MkEmojiPicker.vue
+++ b/packages/client/src/components/MkEmojiPicker.vue
@@ -180,6 +180,11 @@ import { i18n } from "@/i18n";
 import { defaultStore } from "@/store";
 import icon from "@/scripts/icon";
 
+// FIXME: This variable doesn't seem to be used at all. I don't know why it was here.
+const isActive = ref<boolean>();
+
+type EmojiDef = string | entities.CustomEmoji | UnicodeEmojiDef;
+
 const props = withDefaults(
 	defineProps<{
 		showPinned?: boolean;
@@ -193,7 +198,7 @@ const props = withDefaults(
 );
 
 const emit = defineEmits<{
-	(ev: "chosen", v: string, ev: MouseEvent): void;
+	chosen: [v: string, ev?: MouseEvent],
 }>();
 
 const search = ref<HTMLInputElement>();
@@ -411,12 +416,18 @@ function reset() {
 }
 
 function getKey(
-	emoji: string | entities.CustomEmoji | UnicodeEmojiDef,
+	emoji: EmojiDef,
 ): string {
-	return typeof emoji === "string" ? emoji : emoji.emoji || `:${emoji.name}:`;
+	if (typeof emoji === "string") {
+		return emoji;
+	}
+	if ("emoji" in emoji) {
+		return emoji.emoji;
+	}
+	return `:${emoji.name}:`;
 }
 
-function chosen(emoji: any, ev?: MouseEvent) {
+function chosen(emoji: EmojiDef, ev?: MouseEvent) {
 	const el =
 		ev && ((ev.currentTarget ?? ev.target) as HTMLElement | null | undefined);
 	if (el) {
@@ -432,22 +443,33 @@ function chosen(emoji: any, ev?: MouseEvent) {
 	// 最近使った絵文字更新
 	if (!pinned.value.includes(key)) {
 		let recents = defaultStore.state.recentlyUsedEmojis;
-		recents = recents.filter((emoji: any) => emoji !== key);
+		recents = recents.filter((emoji) => emoji !== key);
 		recents.unshift(key);
 		defaultStore.set("recentlyUsedEmojis", recents.splice(0, 32));
 	}
 }
 
-function paste(event: ClipboardEvent) {
-	const paste = (event.clipboardData || window.clipboardData).getData("text");
-	if (done(paste)) {
+async function paste(event: ClipboardEvent) {
+	let pasteStr: string | null = null;
+	if (event.clipboardData) {
+		pasteStr = event.clipboardData.getData("text");
+	} else {
+		// Use native api
+		try {
+			pasteStr = await window.navigator.clipboard.readText();
+		} catch(_err) {
+			// Reading the clipboard requires permission, and the user did not give it
+		}
+	}
+	if (done(pasteStr)) {
 		event.preventDefault();
 	}
 }
 
-function done(query?: any): boolean | void {
+function done(query?: string | null): boolean {
+	// biome-ignore lint/style/noParameterAssign: assign it intentially
 	if (query == null) query = q.value;
-	if (query == null || typeof query !== "string") return;
+	if (query == null || typeof query !== "string") return false;
 
 	const q2 = query.replaceAll(":", "");
 	const exactMatchCustom = customEmojis.find((emoji) => emoji.name === q2);
@@ -470,6 +492,7 @@ function done(query?: any): boolean | void {
 		chosen(searchResultUnicode.value[0]);
 		return true;
 	}
+	return false;
 }
 
 onMounted(() => {

From ef94ba1474be829ee606ec4151624b3df71563d4 Mon Sep 17 00:00:00 2001
From: Lhcfl <Lhcfl@outlook.com>
Date: Fri, 12 Apr 2024 00:20:51 +0800
Subject: [PATCH 024/110] refactor: fix type errrors of some components

---
 packages/client/src/components/MkEmojiPicker.vue       | 8 +++-----
 packages/client/src/components/MkEmojiPickerDialog.vue | 4 ++--
 packages/client/src/components/MkFollowButton.vue      | 2 +-
 packages/client/src/components/MkForgotPassword.vue    | 6 +++---
 packages/client/src/components/MkFormulaCore.vue       | 4 ++--
 5 files changed, 11 insertions(+), 13 deletions(-)

diff --git a/packages/client/src/components/MkEmojiPicker.vue b/packages/client/src/components/MkEmojiPicker.vue
index 4cb9bfad48..afc2f15817 100644
--- a/packages/client/src/components/MkEmojiPicker.vue
+++ b/packages/client/src/components/MkEmojiPicker.vue
@@ -198,7 +198,7 @@ const props = withDefaults(
 );
 
 const emit = defineEmits<{
-	chosen: [v: string, ev?: MouseEvent],
+	chosen: [v: string, ev?: MouseEvent];
 }>();
 
 const search = ref<HTMLInputElement>();
@@ -415,9 +415,7 @@ function reset() {
 	q.value = "";
 }
 
-function getKey(
-	emoji: EmojiDef,
-): string {
+function getKey(emoji: EmojiDef): string {
 	if (typeof emoji === "string") {
 		return emoji;
 	}
@@ -457,7 +455,7 @@ async function paste(event: ClipboardEvent) {
 		// Use native api
 		try {
 			pasteStr = await window.navigator.clipboard.readText();
-		} catch(_err) {
+		} catch (_err) {
 			// Reading the clipboard requires permission, and the user did not give it
 		}
 	}
diff --git a/packages/client/src/components/MkEmojiPickerDialog.vue b/packages/client/src/components/MkEmojiPickerDialog.vue
index 7c3319fb9f..426bbf9469 100644
--- a/packages/client/src/components/MkEmojiPickerDialog.vue
+++ b/packages/client/src/components/MkEmojiPickerDialog.vue
@@ -51,7 +51,7 @@ withDefaults(
 );
 
 const emit = defineEmits<{
-	(ev: "done", v: any): void;
+	(ev: "done", v: string): void;
 	(ev: "close"): void;
 	(ev: "closed"): void;
 }>();
@@ -64,7 +64,7 @@ function checkForShift(ev?: MouseEvent) {
 	modal.value?.close(ev);
 }
 
-function chosen(emoji: any, ev: MouseEvent) {
+function chosen(emoji: string, ev?: MouseEvent) {
 	emit("done", emoji);
 	checkForShift(ev);
 }
diff --git a/packages/client/src/components/MkFollowButton.vue b/packages/client/src/components/MkFollowButton.vue
index ffe1de72af..0920001c7b 100644
--- a/packages/client/src/components/MkFollowButton.vue
+++ b/packages/client/src/components/MkFollowButton.vue
@@ -8,7 +8,7 @@
 		<i :class="icon('ph-dots-three-outline')"></i>
 	</button>
 	<button
-		v-if="!hideFollowButton && isSignedIn && me.id != user.id"
+		v-if="!hideFollowButton && isSignedIn && me!.id != user.id"
 		v-tooltip="full ? null : `${state} ${user.name || user.username}`"
 		class="kpoogebi _button follow-button"
 		:class="{
diff --git a/packages/client/src/components/MkForgotPassword.vue b/packages/client/src/components/MkForgotPassword.vue
index feaaa5f120..2ca1a87e87 100644
--- a/packages/client/src/components/MkForgotPassword.vue
+++ b/packages/client/src/components/MkForgotPassword.vue
@@ -3,7 +3,7 @@
 		ref="dialog"
 		:width="370"
 		:height="400"
-		@close="dialog.close()"
+		@close="dialog!.close()"
 		@closed="emit('closed')"
 	>
 		<template #header>{{ i18n.ts.forgotPassword }}</template>
@@ -76,7 +76,7 @@ const emit = defineEmits<{
 	(ev: "closed"): void;
 }>();
 
-const dialog: InstanceType<typeof XModalWindow> = ref();
+const dialog = ref<InstanceType<typeof XModalWindow> | null>(null);
 
 const username = ref("");
 const email = ref("");
@@ -89,7 +89,7 @@ async function onSubmit() {
 		email: email.value,
 	});
 	emit("done");
-	dialog.value.close();
+	dialog.value!.close();
 }
 </script>
 
diff --git a/packages/client/src/components/MkFormulaCore.vue b/packages/client/src/components/MkFormulaCore.vue
index 2db4c7d00d..d7f251dbcb 100644
--- a/packages/client/src/components/MkFormulaCore.vue
+++ b/packages/client/src/components/MkFormulaCore.vue
@@ -19,10 +19,10 @@ export default defineComponent({
 		},
 	},
 	computed: {
-		compiledFormula(): any {
+		compiledFormula() {
 			const katexString = katex.renderToString(this.formula, {
 				throwOnError: false,
-			} as any);
+			});
 			return this.block
 				? `<div style="text-align:center">${katexString}</div>`
 				: katexString;

From d8b4eb6f5e173be5f0ae1d17bab793e03d036062 Mon Sep 17 00:00:00 2001
From: Lhcfl <Lhcfl@outlook.com>
Date: Fri, 12 Apr 2024 00:22:49 +0800
Subject: [PATCH 025/110] fix: gallery posts not show & fix types

---
 .../src/models/repositories/gallery-post.ts   |  4 ++-
 .../backend/src/models/schema/gallery-post.ts | 12 ++++++-
 .../src/components/MkGalleryPostPreview.vue   | 20 +++++++++--
 packages/client/src/pages/gallery/post.vue    | 34 ++++++++++---------
 packages/firefish-js/src/api.types.ts         | 26 ++++++++++++--
 packages/firefish-js/src/entities.ts          | 16 ++++++++-
 packages/firefish-js/src/streaming.types.ts   |  8 +++--
 7 files changed, 93 insertions(+), 27 deletions(-)

diff --git a/packages/backend/src/models/repositories/gallery-post.ts b/packages/backend/src/models/repositories/gallery-post.ts
index d91fb9de2a..c4078e9091 100644
--- a/packages/backend/src/models/repositories/gallery-post.ts
+++ b/packages/backend/src/models/repositories/gallery-post.ts
@@ -19,7 +19,9 @@ export const GalleryPostRepository = db.getRepository(GalleryPost).extend({
 			createdAt: post.createdAt.toISOString(),
 			updatedAt: post.updatedAt.toISOString(),
 			userId: post.userId,
-			user: Users.pack(post.user || post.userId, me),
+			user: Users.pack(post.user || post.userId, me, {
+				detail: true,
+			}),
 			title: post.title,
 			description: post.description,
 			fileIds: post.fileIds,
diff --git a/packages/backend/src/models/schema/gallery-post.ts b/packages/backend/src/models/schema/gallery-post.ts
index 9ac348e1fb..ae22507643 100644
--- a/packages/backend/src/models/schema/gallery-post.ts
+++ b/packages/backend/src/models/schema/gallery-post.ts
@@ -38,7 +38,7 @@ export const packedGalleryPostSchema = {
 		},
 		user: {
 			type: "object",
-			ref: "UserLite",
+			ref: "UserDetailed",
 			optional: false,
 			nullable: false,
 		},
@@ -79,5 +79,15 @@ export const packedGalleryPostSchema = {
 			optional: false,
 			nullable: false,
 		},
+		isLiked: {
+			type: "boolean",
+			optional: true,
+			nullable: false,
+		},
+		likedCount: {
+			type: "number",
+			optional: false,
+			nullable: false,
+		},
 	},
 } as const;
diff --git a/packages/client/src/components/MkGalleryPostPreview.vue b/packages/client/src/components/MkGalleryPostPreview.vue
index e393c6acc8..cbef1c3d75 100644
--- a/packages/client/src/components/MkGalleryPostPreview.vue
+++ b/packages/client/src/components/MkGalleryPostPreview.vue
@@ -2,10 +2,24 @@
 	<MkA :to="`/gallery/${post.id}`" class="ttasepnz _panel">
 		<div class="thumbnail">
 			<ImgWithBlurhash
+				v-if="post.files && post.files.length > 0"
 				class="img"
 				:src="post.files[0].thumbnailUrl"
 				:hash="post.files[0].blurhash"
 			/>
+			<div
+				v-else
+				class="_fullinfo"
+			>
+				<!-- If there is no picture
+					This can happen if the user deletes the image in the drive
+				-->
+				<img
+					src="/static-assets/badges/not-found.webp"
+					class="img"
+					:alt="i18n.ts.notFound"
+				/>
+			</div>
 		</div>
 		<article>
 			<header>
@@ -20,9 +34,11 @@
 
 <script lang="ts" setup>
 import ImgWithBlurhash from "@/components/MkImgWithBlurhash.vue";
+import { i18n } from "@/i18n";
+import type { entities } from "firefish-js";
 
-const props = defineProps<{
-	post: any;
+defineProps<{
+	post: entities.GalleryPost;
 }>();
 </script>
 
diff --git a/packages/client/src/pages/gallery/post.vue b/packages/client/src/pages/gallery/post.vue
index 6f5e29ed0b..f8c92c1867 100644
--- a/packages/client/src/pages/gallery/post.vue
+++ b/packages/client/src/pages/gallery/post.vue
@@ -22,7 +22,7 @@
 						<div class="body _block">
 							<div class="title">{{ post.title }}</div>
 							<div class="description">
-								<Mfm :text="post.description" />
+								<Mfm :text="post.description || ''" />
 							</div>
 							<div class="info">
 								<i :class="icon('ph-clock')"></i>
@@ -59,7 +59,7 @@
 								<div class="other">
 									<button
 										v-if="
-											isSignedIn && me.id === post.user.id
+											isSignedIn && me!.id === post.user.id
 										"
 										v-tooltip="i18n.ts.toEdit"
 										v-click-anime
@@ -105,7 +105,7 @@
 									<MkAcct :user="post.user" />
 								</div>
 								<MkFollowButton
-									v-if="!me || me.id != post.user.id"
+									v-if="!isSignedIn || me!.id != post.user.id"
 									:user="post.user"
 									:inline="true"
 									:transparent="false"
@@ -140,7 +140,7 @@
 							</MkPagination>
 						</MkContainer>
 					</div>
-					<MkError v-else-if="error" @retry="fetch()" />
+					<MkError v-else-if="error" @retry="fetchPost()" />
 					<MkLoading v-else />
 				</transition>
 			</div>
@@ -163,7 +163,8 @@ import { definePageMetadata } from "@/scripts/page-metadata";
 import { shareAvailable } from "@/scripts/share-available";
 import { defaultStore } from "@/store";
 import icon from "@/scripts/icon";
-import { isSignedIn } from "@/me";
+import { isSignedIn, me } from "@/me";
+import type { entities } from "firefish-js";
 
 const router = useRouter();
 
@@ -171,18 +172,19 @@ const props = defineProps<{
 	postId: string;
 }>();
 
-const post = ref(null);
+const post = ref<entities.GalleryPost | null>(null);
 const error = ref(null);
 const otherPostsPagination = {
 	endpoint: "users/gallery/posts" as const,
 	limit: 6,
 	params: computed(() => ({
-		userId: post.value.user.id,
+		userId: post.value!.user.id,
 	})),
 };
 
 function fetchPost() {
 	post.value = null;
+	error.value = null;
 	os.api("gallery/posts/show", {
 		postId: props.postId,
 	})
@@ -196,15 +198,15 @@ function fetchPost() {
 
 function share() {
 	navigator.share({
-		title: post.value.title,
-		text: post.value.description,
-		url: `${url}/gallery/${post.value.id}`,
+		title: post.value!.title,
+		text: post.value!.description || undefined,
+		url: `${url}/gallery/${post.value!.id}`,
 	});
 }
 
 function shareWithNote() {
 	os.post({
-		initialText: `${post.value.title} ${url}/gallery/${post.value.id}`,
+		initialText: `${post.value!.title} ${url}/gallery/${post.value!.id}`,
 	});
 }
 
@@ -212,8 +214,8 @@ function like() {
 	os.api("gallery/posts/like", {
 		postId: props.postId,
 	}).then(() => {
-		post.value.isLiked = true;
-		post.value.likedCount++;
+		post.value!.isLiked = true;
+		post.value!.likedCount++;
 	});
 }
 
@@ -221,13 +223,13 @@ async function unlike() {
 	os.api("gallery/posts/unlike", {
 		postId: props.postId,
 	}).then(() => {
-		post.value.isLiked = false;
-		post.value.likedCount--;
+		post.value!.isLiked = false;
+		post.value!.likedCount--;
 	});
 }
 
 function edit() {
-	router.push(`/gallery/${post.value.id}/edit`);
+	router.push(`/gallery/${post.value!.id}/edit`);
 }
 
 watch(() => props.postId, fetchPost, { immediate: true });
diff --git a/packages/firefish-js/src/api.types.ts b/packages/firefish-js/src/api.types.ts
index 9d1123e7ad..3571d79546 100644
--- a/packages/firefish-js/src/api.types.ts
+++ b/packages/firefish-js/src/api.types.ts
@@ -432,7 +432,12 @@ export type Endpoints = {
 	"gallery/posts/create": { req: TODO; res: TODO };
 	"gallery/posts/delete": { req: { postId: GalleryPost["id"] }; res: null };
 	"gallery/posts/like": { req: TODO; res: TODO };
-	"gallery/posts/show": { req: TODO; res: TODO };
+	"gallery/posts/show": {
+		req: {
+			postId: GalleryPost["id"];
+		};
+		res: GalleryPost;
+	};
 	"gallery/posts/unlike": { req: TODO; res: TODO };
 	"gallery/posts/update": { req: TODO; res: TODO };
 
@@ -474,7 +479,14 @@ export type Endpoints = {
 		res: NoteFavorite[];
 	};
 	"i/gallery/likes": { req: TODO; res: TODO };
-	"i/gallery/posts": { req: TODO; res: TODO };
+	"i/gallery/posts": {
+		req: {
+			limit?: number;
+			sinceId?: NoteFavorite["id"];
+			untilId?: NoteFavorite["id"];
+		};
+		res: GalleryPost[];
+	};
 	"i/get-word-muted-notes-count": { req: TODO; res: TODO };
 	"i/import-following": { req: TODO; res: TODO };
 	"i/import-user-lists": { req: TODO; res: TODO };
@@ -890,7 +902,15 @@ export type Endpoints = {
 		};
 		res: FollowingFolloweePopulated[];
 	};
-	"users/gallery/posts": { req: TODO; res: TODO };
+	"users/gallery/posts": {
+		req: {
+			userId: User["id"];
+			limit?: number;
+			sinceId?: NoteFavorite["id"];
+			untilId?: NoteFavorite["id"];
+		};
+		res: GalleryPost[];
+	};
 	"users/get-frequently-replied-users": { req: TODO; res: TODO };
 	"users/groups/create": { req: TODO; res: TODO };
 	"users/groups/delete": { req: { groupId: UserGroup["id"] }; res: null };
diff --git a/packages/firefish-js/src/entities.ts b/packages/firefish-js/src/entities.ts
index cafbdc7864..35dff298db 100644
--- a/packages/firefish-js/src/entities.ts
+++ b/packages/firefish-js/src/entities.ts
@@ -136,7 +136,21 @@ export type DriveFile = {
 
 export type DriveFolder = TODO;
 
-export type GalleryPost = TODO;
+export type GalleryPost = {
+	id: ID;
+	createdAt: DateString;
+	updatedAt: DateString;
+	title: string;
+	description: string | null;
+	userId: User["id"];
+	user: UserDetailed;
+	fileIds?: DriveFile["id"][];
+	files?: DriveFile[];
+	tags?: string[];
+	isSensitive: boolean;
+	isLiked?: boolean;
+	likedCount: number;
+};
 
 export type Note = {
 	id: ID;
diff --git a/packages/firefish-js/src/streaming.types.ts b/packages/firefish-js/src/streaming.types.ts
index 18c7682608..5b81780271 100644
--- a/packages/firefish-js/src/streaming.types.ts
+++ b/packages/firefish-js/src/streaming.types.ts
@@ -8,7 +8,9 @@ import type {
 	Notification,
 	PageEvent,
 	User,
+	UserDetailed,
 	UserGroup,
+	UserLite,
 } from "./entities";
 import type { Connection } from "./streaming";
 
@@ -26,9 +28,9 @@ export type Channels = {
 			mention: (payload: Note) => void;
 			reply: (payload: Note) => void;
 			renote: (payload: Note) => void;
-			follow: (payload: User) => void; // 自分が他人をフォローしたとき
-			followed: (payload: User) => void; // 他人が自分をフォローしたとき
-			unfollow: (payload: User) => void; // 自分が他人をフォロー解除したとき
+			follow: (payload: UserDetailed) => void; // 自分が他人をフォローしたとき
+			followed: (payload: UserLite) => void; // 他人が自分をフォローしたとき
+			unfollow: (payload: UserDetailed) => void; // 自分が他人をフォロー解除したとき
 			meUpdated: (payload: MeDetailed) => void;
 			pageEvent: (payload: PageEvent) => void;
 			urlUploadFinished: (payload: { marker: string; file: DriveFile }) => void;

From e6b7eca77501cf41959e567a7a95955b3ced29dc Mon Sep 17 00:00:00 2001
From: Lhcfl <Lhcfl@outlook.com>
Date: Fri, 12 Apr 2024 01:12:18 +0800
Subject: [PATCH 026/110] fix type errors of components

---
 .../client/src/components/MkImageViewer.vue   | 14 ++--
 .../src/components/MkImgWithBlurhash.vue      |  8 +-
 .../src/components/MkInstanceCardMini.vue     |  7 +-
 .../src/components/MkInstanceSelectDialog.vue |  8 +-
 .../client/src/components/MkInstanceStats.vue | 81 ++++++++++---------
 .../client/src/components/MkLaunchPad.vue     |  4 +-
 .../client/src/components/global/MkEmoji.vue  |  4 +-
 .../client/src/scripts/use-chart-tooltip.ts   | 15 +++-
 packages/firefish-js/src/api.types.ts         | 11 +++
 packages/firefish-js/src/entities.ts          |  2 +
 10 files changed, 88 insertions(+), 66 deletions(-)

diff --git a/packages/client/src/components/MkImageViewer.vue b/packages/client/src/components/MkImageViewer.vue
index c65136718d..27c4d421aa 100644
--- a/packages/client/src/components/MkImageViewer.vue
+++ b/packages/client/src/components/MkImageViewer.vue
@@ -2,16 +2,16 @@
 	<MkModal
 		ref="modal"
 		:z-priority="'middle'"
-		@click="modal.close()"
+		@click="modal!.close()"
 		@closed="emit('closed')"
 	>
 		<div class="xubzgfga">
 			<header>{{ image.name }}</header>
 			<img
 				:src="image.url"
-				:alt="image.comment"
-				:title="image.comment"
-				@click="modal.close()"
+				:alt="image.comment || undefined"
+				:title="image.comment || undefined"
+				@click="modal!.close()"
 			/>
 			<footer>
 				<span>{{ image.type }}</span>
@@ -33,7 +33,7 @@ import bytes from "@/filters/bytes";
 import number from "@/filters/number";
 import MkModal from "@/components/MkModal.vue";
 
-const props = withDefaults(
+withDefaults(
 	defineProps<{
 		image: entities.DriveFile;
 	}>(),
@@ -41,10 +41,10 @@ const props = withDefaults(
 );
 
 const emit = defineEmits<{
-	(ev: "closed"): void;
+	closed: [];
 }>();
 
-const modal = ref<InstanceType<typeof MkModal>>();
+const modal = ref<InstanceType<typeof MkModal> | null>(null);
 </script>
 
 <style lang="scss" scoped>
diff --git a/packages/client/src/components/MkImgWithBlurhash.vue b/packages/client/src/components/MkImgWithBlurhash.vue
index 5e70515a93..31aa0e1842 100644
--- a/packages/client/src/components/MkImgWithBlurhash.vue
+++ b/packages/client/src/components/MkImgWithBlurhash.vue
@@ -4,20 +4,20 @@
 		ref="canvas"
 		:width="size"
 		:height="size"
-		:title="title"
+		:title="title || undefined"
 	/>
 	<img
 		v-if="src"
 		:src="src"
-		:title="title"
+		:title="title || undefined"
 		:type="type"
-		:alt="alt"
+		:alt="alt || undefined"
 		:class="{
 			cover,
 			wide: largestDimension === 'width',
 			tall: largestDimension === 'height',
 		}"
-		:style="{ 'object-fit': cover ? 'cover' : null }"
+		:style="{ 'object-fit': cover ? 'cover' : undefined }"
 		loading="lazy"
 		@load="onLoad"
 	/>
diff --git a/packages/client/src/components/MkInstanceCardMini.vue b/packages/client/src/components/MkInstanceCardMini.vue
index c44cfa291f..7c722f07f2 100644
--- a/packages/client/src/components/MkInstanceCardMini.vue
+++ b/packages/client/src/components/MkInstanceCardMini.vue
@@ -23,17 +23,14 @@
 </template>
 
 <script lang="ts" setup>
-import { ref } from "vue";
-
 import type { entities } from "firefish-js";
-import * as os from "@/os";
 import { getProxiedImageUrlNullable } from "@/scripts/media-proxy";
 
-const props = defineProps<{
+defineProps<{
 	instance: entities.Instance;
 }>();
 
-function getInstanceIcon(instance): string {
+function getInstanceIcon(instance: entities.Instance): string {
 	return (
 		getProxiedImageUrlNullable(instance.faviconUrl, "preview") ??
 		getProxiedImageUrlNullable(instance.iconUrl, "preview") ??
diff --git a/packages/client/src/components/MkInstanceSelectDialog.vue b/packages/client/src/components/MkInstanceSelectDialog.vue
index 92c060e4dd..e1e0da550c 100644
--- a/packages/client/src/components/MkInstanceSelectDialog.vue
+++ b/packages/client/src/components/MkInstanceSelectDialog.vue
@@ -65,14 +65,14 @@ import * as os from "@/os";
 import { i18n } from "@/i18n";
 
 const emit = defineEmits<{
-	(ev: "ok", selected: entities.Instance): void;
-	(ev: "cancel"): void;
-	(ev: "closed"): void;
+	ok: [selected: entities.Instance];
+	cancel: [];
+	closed: [];
 }>();
 
 const hostname = ref("");
 const instances = ref<entities.Instance[]>([]);
-const selected = ref<entities.Instance | null>();
+const selected = ref<entities.Instance | null>(null);
 const dialogEl = ref<InstanceType<typeof XModalWindow>>();
 
 let searchOrderLatch = 0;
diff --git a/packages/client/src/components/MkInstanceStats.vue b/packages/client/src/components/MkInstanceStats.vue
index 7ae2278d9f..72f987ec84 100644
--- a/packages/client/src/components/MkInstanceStats.vue
+++ b/packages/client/src/components/MkInstanceStats.vue
@@ -52,6 +52,7 @@ import { i18n } from "@/i18n";
 import MkActiveUsersHeatmap from "@/components/MkActiveUsersHeatmap.vue";
 import MkFolder from "@/components/MkFolder.vue";
 import { initChart } from "@/scripts/init-chart";
+import type { entities } from "firefish-js";
 
 initChart();
 
@@ -67,7 +68,18 @@ const { handler: externalTooltipHandler2 } = useChartTooltip({
 	position: "middle",
 });
 
-function createDoughnut(chartEl, tooltip, data) {
+interface ColorData {
+	name: string;
+	color: string | undefined;
+	value: number;
+	onClick?: () => void;
+}
+
+function createDoughnut(
+	chartEl: HTMLCanvasElement,
+	tooltip: typeof externalTooltipHandler1,
+	data: ColorData[],
+) {
 	const chartInstance = new Chart(chartEl, {
 		type: "doughnut",
 		data: {
@@ -96,13 +108,13 @@ function createDoughnut(chartEl, tooltip, data) {
 			},
 			onClick: (ev) => {
 				const hit = chartInstance.getElementsAtEventForMode(
-					ev,
+					ev as unknown as Event,
 					"nearest",
 					{ intersect: true },
 					false,
 				)[0];
-				if (hit && data[hit.index].onClick) {
-					data[hit.index].onClick();
+				if (hit) {
+					data[hit.index].onClick?.();
 				}
 			},
 			plugins: {
@@ -124,48 +136,41 @@ function createDoughnut(chartEl, tooltip, data) {
 	return chartInstance;
 }
 
+function instance2ColorData(x: entities.Instance): ColorData {
+	return {
+		name: x.host,
+		color: x.themeColor || undefined,
+		value: x.followersCount,
+		onClick: () => {
+			os.pageWindow(`/instance-info/${x.host}`);
+		},
+	};
+}
+
 onMounted(() => {
 	os.apiGet("federation/stats", { limit: 30 }).then((fedStats) => {
 		createDoughnut(
-			subDoughnutEl.value,
+			subDoughnutEl.value!,
 			externalTooltipHandler1,
-			fedStats.topSubInstances
-				.map((x) => ({
-					name: x.host,
-					color: x.themeColor,
-					value: x.followersCount,
-					onClick: () => {
-						os.pageWindow(`/instance-info/${x.host}`);
-					},
-				}))
-				.concat([
-					{
-						name: "(other)",
-						color: "#80808080",
-						value: fedStats.otherFollowersCount,
-					},
-				]),
+			fedStats.topSubInstances.map(instance2ColorData).concat([
+				{
+					name: "(other)",
+					color: "#80808080",
+					value: fedStats.otherFollowersCount,
+				},
+			]),
 		);
 
 		createDoughnut(
-			pubDoughnutEl.value,
+			pubDoughnutEl.value!,
 			externalTooltipHandler2,
-			fedStats.topPubInstances
-				.map((x) => ({
-					name: x.host,
-					color: x.themeColor,
-					value: x.followingCount,
-					onClick: () => {
-						os.pageWindow(`/instance-info/${x.host}`);
-					},
-				}))
-				.concat([
-					{
-						name: "(other)",
-						color: "#80808080",
-						value: fedStats.otherFollowingCount,
-					},
-				]),
+			fedStats.topPubInstances.map(instance2ColorData).concat([
+				{
+					name: "(other)",
+					color: "#80808080",
+					value: fedStats.otherFollowingCount,
+				},
+			]),
 		);
 	});
 });
diff --git a/packages/client/src/components/MkLaunchPad.vue b/packages/client/src/components/MkLaunchPad.vue
index ddb4dc69d2..d17857dc43 100644
--- a/packages/client/src/components/MkLaunchPad.vue
+++ b/packages/client/src/components/MkLaunchPad.vue
@@ -6,7 +6,7 @@
 		:anchor="anchor"
 		:transparent-bg="true"
 		:src="src"
-		@click="modal.close()"
+		@click="modal!.close()"
 		@closed="emit('closed')"
 	>
 		<div
@@ -109,7 +109,7 @@ const items = Object.keys(navbarItemDef)
 	}));
 
 function close() {
-	modal.value.close();
+	modal.value!.close();
 }
 </script>
 
diff --git a/packages/client/src/components/global/MkEmoji.vue b/packages/client/src/components/global/MkEmoji.vue
index 86e3d344b6..57380ba6f4 100644
--- a/packages/client/src/components/global/MkEmoji.vue
+++ b/packages/client/src/components/global/MkEmoji.vue
@@ -54,8 +54,8 @@ const url = computed(() => {
 		return char2filePath(char.value);
 	} else {
 		return defaultStore.state.disableShowingAnimatedImages
-			? getStaticImageUrl(customEmoji.value.url)
-			: customEmoji.value.url;
+			? getStaticImageUrl(customEmoji.value!.url)
+			: customEmoji.value!.url;
 	}
 });
 const alt = computed(() =>
diff --git a/packages/client/src/scripts/use-chart-tooltip.ts b/packages/client/src/scripts/use-chart-tooltip.ts
index 573b5b4c7c..515c60a179 100644
--- a/packages/client/src/scripts/use-chart-tooltip.ts
+++ b/packages/client/src/scripts/use-chart-tooltip.ts
@@ -1,6 +1,13 @@
 import { onDeactivated, onUnmounted, ref } from "vue";
 import * as os from "@/os";
 import MkChartTooltip from "@/components/MkChartTooltip.vue";
+import type { Color, TooltipOptions } from "chart.js";
+
+type ToolTipSerie = {
+	backgroundColor: Color;
+	borderColor: Color;
+	text: string;
+};
 
 export function useChartTooltip(
 	opts: { position: "top" | "middle" } = { position: "top" },
@@ -8,9 +15,9 @@ export function useChartTooltip(
 	const tooltipShowing = ref(false);
 	const tooltipX = ref(0);
 	const tooltipY = ref(0);
-	const tooltipTitle = ref(null);
-	const tooltipSeries = ref(null);
-	let disposeTooltipComponent;
+	const tooltipTitle = ref<string | null>(null);
+	const tooltipSeries = ref<ToolTipSerie[] | null>(null);
+	let disposeTooltipComponent: () => void;
 
 	os.popup(
 		MkChartTooltip,
@@ -34,7 +41,7 @@ export function useChartTooltip(
 		tooltipShowing.value = false;
 	});
 
-	function handler(context) {
+	const handler: TooltipOptions["external"] = (context) => {
 		if (context.tooltip.opacity === 0) {
 			tooltipShowing.value = false;
 			return;
diff --git a/packages/firefish-js/src/api.types.ts b/packages/firefish-js/src/api.types.ts
index 3571d79546..efa97d0a96 100644
--- a/packages/firefish-js/src/api.types.ts
+++ b/packages/firefish-js/src/api.types.ts
@@ -405,6 +405,17 @@ export type Endpoints = {
 		res: Instance[];
 	};
 	"federation/show-instance": { req: { host: string }; res: Instance };
+	"federation/stats": {
+		req: {
+			limit?: number;
+		};
+		res: {
+			topSubInstances: Instance[];
+			otherFollowersCount: number;
+			topPubInstances: Instance[];
+			otherFollowingCount: number;
+		};
+	};
 	"federation/update-remote-user": { req: { userId: User["id"] }; res: null };
 	"federation/users": {
 		req: {
diff --git a/packages/firefish-js/src/entities.ts b/packages/firefish-js/src/entities.ts
index 35dff298db..7bf1183c2d 100644
--- a/packages/firefish-js/src/entities.ts
+++ b/packages/firefish-js/src/entities.ts
@@ -539,6 +539,8 @@ export type Instance = {
 	lastCommunicatedAt: DateString;
 	isNotResponding: boolean;
 	isSuspended: boolean;
+	isBlocked: boolean;
+	isSilenced: boolean;
 	softwareName: string | null;
 	softwareVersion: string | null;
 	openRegistrations: boolean | null;

From 38668c4c115cf0b406012de240a693f944c6c64d Mon Sep 17 00:00:00 2001
From: Lhcfl <Lhcfl@outlook.com>
Date: Fri, 12 Apr 2024 01:32:04 +0800
Subject: [PATCH 027/110] fix type errors

---
 .../src/components/MkInstanceTicker.vue       |  6 ++--
 .../src/components/MkManyAnnouncements.vue    |  2 +-
 packages/client/src/components/MkMedia.vue    |  2 +-
 .../client/src/components/MkMediaBanner.vue   |  2 +-
 .../client/src/scripts/use-chart-tooltip.ts   |  2 +-
 packages/firefish-js/src/entities.ts          | 35 ++++++++++++++++++-
 6 files changed, 41 insertions(+), 8 deletions(-)

diff --git a/packages/client/src/components/MkInstanceTicker.vue b/packages/client/src/components/MkInstanceTicker.vue
index 46da872c2c..498303ef8e 100644
--- a/packages/client/src/components/MkInstanceTicker.vue
+++ b/packages/client/src/components/MkInstanceTicker.vue
@@ -35,12 +35,12 @@ const ticker = ref<HTMLElement | null>(null);
 
 // if no instance data is given, this is for the local instance
 const instance = props.instance ?? {
-	faviconUrl: Instance.faviconUrl || Instance.iconUrl || "/favicon.ico",
+	faviconUrl: Instance.iconUrl || "/favicon.ico",
 	name: instanceName,
 	themeColor: (
 		document.querySelector('meta[name="theme-color-orig"]') as HTMLMetaElement
 	)?.content,
-	softwareName: Instance.softwareName ?? "Firefish",
+	softwareName: "Firefish",
 	softwareVersion: version,
 };
 
@@ -67,7 +67,7 @@ const commonNames = new Map<string, string>([
 	["wxwclub", "wxwClub"],
 ]);
 
-const capitalize = (s: string) => {
+const capitalize = (s?: string | null) => {
 	if (s == null) return "Unknown";
 	if (commonNames.has(s)) return commonNames.get(s);
 	return s[0].toUpperCase() + s.slice(1);
diff --git a/packages/client/src/components/MkManyAnnouncements.vue b/packages/client/src/components/MkManyAnnouncements.vue
index c65cd0a9ab..903891b64c 100644
--- a/packages/client/src/components/MkManyAnnouncements.vue
+++ b/packages/client/src/components/MkManyAnnouncements.vue
@@ -23,7 +23,7 @@ import { i18n } from "@/i18n";
 
 const modal = shallowRef<InstanceType<typeof MkModal>>();
 const checkAnnouncements = () => {
-	modal.value.close();
+	modal.value!.close();
 	location.href = "/announcements";
 };
 </script>
diff --git a/packages/client/src/components/MkMedia.vue b/packages/client/src/components/MkMedia.vue
index ef8912f138..c05a11e448 100644
--- a/packages/client/src/components/MkMedia.vue
+++ b/packages/client/src/components/MkMedia.vue
@@ -50,7 +50,7 @@
 			>
 				<video
 					:poster="media.thumbnailUrl"
-					:aria-label="media.comment"
+					:aria-label="media.comment || undefined"
 					preload="none"
 					controls
 					playsinline
diff --git a/packages/client/src/components/MkMediaBanner.vue b/packages/client/src/components/MkMediaBanner.vue
index d703fde470..cee8425c36 100644
--- a/packages/client/src/components/MkMediaBanner.vue
+++ b/packages/client/src/components/MkMediaBanner.vue
@@ -64,7 +64,7 @@ import "vue-plyr/dist/vue-plyr.css";
 import { i18n } from "@/i18n";
 import icon from "@/scripts/icon";
 
-const props = withDefaults(
+withDefaults(
 	defineProps<{
 		media: entities.DriveFile;
 	}>(),
diff --git a/packages/client/src/scripts/use-chart-tooltip.ts b/packages/client/src/scripts/use-chart-tooltip.ts
index 515c60a179..bc6c5b1eeb 100644
--- a/packages/client/src/scripts/use-chart-tooltip.ts
+++ b/packages/client/src/scripts/use-chart-tooltip.ts
@@ -63,7 +63,7 @@ export function useChartTooltip(
 		} else if (opts.position === "middle") {
 			tooltipY.value = rect.top + window.scrollY + context.tooltip.caretY;
 		}
-	}
+	};
 
 	return {
 		handler,
diff --git a/packages/firefish-js/src/entities.ts b/packages/firefish-js/src/entities.ts
index 7bf1183c2d..34c958ea5f 100644
--- a/packages/firefish-js/src/entities.ts
+++ b/packages/firefish-js/src/entities.ts
@@ -371,7 +371,40 @@ export type LiteInstanceMetadata = {
 };
 
 export type DetailedInstanceMetadata = LiteInstanceMetadata & {
-	features: Record<string, any>;
+	features: {
+		registration: boolean;
+		localTimeLine: boolean;
+		recommendedTimeLine: boolean;
+		globalTimeLine: boolean;
+		searchFilters: boolean;
+		hcaptcha: boolean;
+		recaptcha: boolean;
+		objectStorage: boolean;
+		serviceWorker: boolean;
+		miauth?: boolean;
+	};
+	langs: string[];
+	moreUrls: object;
+	repositoryUrl: string;
+	feedbackUrl: string;
+	defaultDarkTheme: string | null;
+	defaultLightTheme: string | null;
+	enableGuestTimeline: boolean;
+	cacheRemoteFiles: boolean;
+	emailRequiredForSignup: boolean;
+	mascotImageUrl: string;
+	bannerUrl: string;
+	errorImageUrl: string;
+	iconUrl: string | null;
+	maxCaptionTextLength: number;
+	requireSetup: boolean;
+	translatorAvailable: boolean;
+	proxyAccountName: string | null;
+	secureMode?: boolean;
+	privateMode?: boolean;
+	defaultReaction: string;
+	donationLink?: string | null;
+	enableServerMachineStats?: boolean;
 };
 
 export type InstanceMetadata = LiteInstanceMetadata | DetailedInstanceMetadata;

From 9c75dd0b263b5458dbe6234212a20ec0948e5a9c Mon Sep 17 00:00:00 2001
From: Lhcfl <Lhcfl@outlook.com>
Date: Fri, 12 Apr 2024 09:33:27 +0800
Subject: [PATCH 028/110] refactor: define more cleared type of os.ts

---
 packages/client/src/components/MkLink.vue |  2 +-
 packages/client/src/os.ts                 | 28 ++++++++++++++++++-----
 2 files changed, 23 insertions(+), 7 deletions(-)

diff --git a/packages/client/src/components/MkLink.vue b/packages/client/src/components/MkLink.vue
index 4add7c32da..34db74397e 100644
--- a/packages/client/src/components/MkLink.vue
+++ b/packages/client/src/components/MkLink.vue
@@ -42,7 +42,7 @@ useTooltip(el, (showing) => {
 	os.popup(
 		defineAsyncComponent(() => import("@/components/MkUrlPreviewPopup.vue")),
 		{
-			showing,
+			showing: showing.value,
 			url: props.url,
 			source: el.value,
 		},
diff --git a/packages/client/src/os.ts b/packages/client/src/os.ts
index 328b961b21..646f627d54 100644
--- a/packages/client/src/os.ts
+++ b/packages/client/src/os.ts
@@ -3,7 +3,13 @@
 import { EventEmitter } from "eventemitter3";
 import { type entities, api as firefishApi } from "firefish-js";
 import insertTextAtCursor from "insert-text-at-cursor";
-import type { Component, Ref } from "vue";
+import type {
+	Component,
+	ComponentPublicInstance,
+	DefineComponent,
+	EmitsOptions,
+	Ref,
+} from "vue";
 import { defineAsyncComponent, markRaw, ref } from "vue";
 import { i18n } from "./i18n";
 import MkDialog from "@/components/MkDialog.vue";
@@ -196,13 +202,23 @@ export function claimZIndex(
 
 let uniqueId = 0;
 export function getUniqueId(): string {
-	return uniqueId++ + "";
+	return String(uniqueId++);
 }
 
-export async function popup(
-	component: Component,
-	props: Record<string, any>,
-	events = {},
+interface VueComponentConstructor<P, E> {
+	__isFragment?: never;
+	__isTeleport?: never;
+	__isSuspense?: never;
+	new (): {
+		$props: P;
+	};
+	emits?: E;
+}
+
+export async function popup<Props, Emits>(
+	component: VueComponentConstructor<Props, Emits>,
+	props: Props & Record<string, unknown>,
+	events: Partial<NonNullable<Emits>> | Record<string, never> = {},
 	disposeEvent?: string,
 ) {
 	markRaw(component);

From 95b91c639637f686999a1ae20d405ca15518b5d3 Mon Sep 17 00:00:00 2001
From: Lhcfl <Lhcfl@outlook.com>
Date: Fri, 12 Apr 2024 09:40:34 +0800
Subject: [PATCH 029/110] chore: remove unused type imports

---
 packages/client/src/os.ts | 15 +++++----------
 1 file changed, 5 insertions(+), 10 deletions(-)

diff --git a/packages/client/src/os.ts b/packages/client/src/os.ts
index 646f627d54..426fd29084 100644
--- a/packages/client/src/os.ts
+++ b/packages/client/src/os.ts
@@ -5,9 +5,6 @@ import { type entities, api as firefishApi } from "firefish-js";
 import insertTextAtCursor from "insert-text-at-cursor";
 import type {
 	Component,
-	ComponentPublicInstance,
-	DefineComponent,
-	EmitsOptions,
 	Ref,
 } from "vue";
 import { defineAsyncComponent, markRaw, ref } from "vue";
@@ -180,13 +177,11 @@ export function promiseDialog<T>(
 }
 
 let popupIdCount = 0;
-export const popups = ref([]) as Ref<
-	{
-		id: any;
-		component: any;
-		props: Record<string, any>;
-	}[]
->;
+export const popups = ref<{
+	id: number;
+	component: Component;
+	props: Record<string, unknown>;
+}[]>([]);
 
 const zIndexes = {
 	low: 1000000,

From f3b189a70c3f62faf111b7b8c62eb845642bf0bc Mon Sep 17 00:00:00 2001
From: Lhcfl <Lhcfl@outlook.com>
Date: Fri, 12 Apr 2024 09:46:04 +0800
Subject: [PATCH 030/110] refactor: rewrite MkMediaCaption

---
 .../client/src/components/MkMediaCaption.vue  | 190 ++++++++----------
 packages/firefish-js/src/api.types.ts         |   6 +
 2 files changed, 90 insertions(+), 106 deletions(-)

diff --git a/packages/client/src/components/MkMediaCaption.vue b/packages/client/src/components/MkMediaCaption.vue
index 88bf24631b..815b7c5adf 100644
--- a/packages/client/src/components/MkMediaCaption.vue
+++ b/packages/client/src/components/MkMediaCaption.vue
@@ -1,5 +1,5 @@
 <template>
-	<MkModal ref="modal" @click="done(true)" @closed="$emit('closed')">
+	<MkModal ref="modal" @click="done(true)" @closed="emit('closed')">
 		<div class="container">
 			<div class="fullwidth top-caption">
 				<div class="mk-dialog">
@@ -48,9 +48,9 @@
 				<img
 					id="imgtocaption"
 					:src="image.url"
-					:alt="image.comment"
-					:title="image.comment"
-					@click="$refs.modal.close()"
+					:alt="image.comment || undefined"
+					:title="image.comment || undefined"
+					@click="modal!.close()"
 				/>
 				<footer>
 					<span>{{ image.type }}</span>
@@ -65,8 +65,8 @@
 	</MkModal>
 </template>
 
-<script lang="ts">
-import { defineComponent } from "vue";
+<script lang="ts" setup>
+import { computed, onBeforeUnmount, onMounted, ref } from "vue";
 import insertTextAtCursor from "insert-text-at-cursor";
 import { length } from "stringz";
 import * as os from "@/os";
@@ -76,122 +76,100 @@ import bytes from "@/filters/bytes";
 import number from "@/filters/number";
 import { i18n } from "@/i18n";
 import { instance } from "@/instance";
+import type { entities } from "firefish-js";
 
-export default defineComponent({
-	components: {
-		MkModal,
-		MkButton,
-	},
-
-	props: {
-		image: {
-			type: Object,
-			required: true,
-		},
-		title: {
-			type: String,
-			required: false,
-		},
+const props = withDefaults(
+	defineProps<{
+		image: entities.DriveFile;
 		input: {
-			required: true,
-		},
-		showOkButton: {
-			type: Boolean,
-			default: true,
-		},
-		showCaptionButton: {
-			type: Boolean,
-			default: true,
-		},
-		showCancelButton: {
-			type: Boolean,
-			default: true,
-		},
-		cancelableByBgClick: {
-			type: Boolean,
-			default: true,
-		},
-	},
-
-	emits: ["done", "closed"],
-
-	data() {
-		return {
-			inputValue: this.input.default ? this.input.default : null,
-			i18n,
+			placeholder: string;
+			default: string;
 		};
+		title?: string;
+		showOkButton?: boolean;
+		showCaptionButton?: boolean;
+		showCancelButton?: boolean;
+		cancelableByBgClick?: boolean;
+	}>(),
+	{
+		showOkButton: true,
+		showCaptionButton: true,
+		showCancelButton: true,
+		cancelableByBgClick: true,
 	},
+);
 
-	computed: {
-		remainingLength(): number {
-			const maxCaptionLength = instance.maxCaptionTextLength ?? 512;
-			if (typeof this.inputValue !== "string") return maxCaptionLength;
-			return maxCaptionLength - length(this.inputValue);
-		},
-	},
+const emit = defineEmits<{
+	done: [v: { canceled: boolean; result?: string | null }];
+	closed: [];
+}>();
 
-	mounted() {
-		document.addEventListener("keydown", this.onKeydown);
-	},
+const modal = ref<InstanceType<typeof MkModal> | null>(null);
 
-	beforeUnmount() {
-		document.removeEventListener("keydown", this.onKeydown);
-	},
+const inputValue = ref(props.input.default ? props.input.default : null);
 
-	methods: {
-		bytes,
-		number,
+const remainingLength = computed(() => {
+	const maxCaptionLength = instance.maxCaptionTextLength ?? 512;
+	if (typeof inputValue.value !== "string") return maxCaptionLength;
+	return maxCaptionLength - length(inputValue.value);
+});
 
-		done(canceled, result?) {
-			this.$emit("done", { canceled, result });
-			this.$refs.modal.close();
-		},
+function done(canceled: boolean, result?: string | null) {
+	emit("done", { canceled, result });
+	modal.value!.close();
+}
 
-		async ok() {
-			if (!this.showOkButton) return;
+async function ok() {
+	if (!props.showOkButton) return;
 
-			const result = this.inputValue;
-			this.done(false, result);
-		},
+	const result = inputValue.value;
+	done(false, result);
+}
 
-		cancel() {
-			this.done(true);
-		},
+function cancel() {
+	done(true);
+}
 
-		onBgClick() {
-			if (this.cancelableByBgClick) {
-				this.cancel();
-			}
-		},
+// function onBgClick() {
+// 	if (props.cancelableByBgClick) {
+// 		cancel();
+// 	}
+// }
 
-		onKeydown(evt) {
-			if (evt.which === 27) {
-				// ESC
-				this.cancel();
-			}
-		},
+function onKeydown(evt) {
+	if (evt.which === 27) {
+		// ESC
+		cancel();
+	}
+}
 
-		onInputKeydown(evt) {
-			if (evt.which === 13) {
-				// Enter
-				if (evt.ctrlKey) {
-					evt.preventDefault();
-					evt.stopPropagation();
-					this.ok();
-				}
-			}
-		},
+function onInputKeydown(evt) {
+	if (evt.which === 13) {
+		// Enter
+		if (evt.ctrlKey) {
+			evt.preventDefault();
+			evt.stopPropagation();
+			ok();
+		}
+	}
+}
 
-		caption() {
-			const img = document.getElementById("imgtocaption") as HTMLImageElement;
-			const ta = document.getElementById("captioninput") as HTMLTextAreaElement;
-			os.api("drive/files/caption-image", {
-				url: img.src,
-			}).then((text) => {
-				insertTextAtCursor(ta, text.slice(0, 512 - ta.value.length));
-			});
-		},
-	},
+function caption() {
+	const img = document.getElementById("imgtocaption") as HTMLImageElement;
+	const ta = document.getElementById("captioninput") as HTMLTextAreaElement;
+	os.api("drive/files/caption-image", {
+		url: img.src,
+	}).then((text) => {
+		insertTextAtCursor(ta, text.slice(0, 512 - ta.value.length));
+	});
+}
+
+onMounted(() => {
+	document.addEventListener("keydown", onKeydown);
+});
+
+onBeforeUnmount(() => {
+	document.removeEventListener("keydown", onKeydown);
 });
 </script>
 
diff --git a/packages/firefish-js/src/api.types.ts b/packages/firefish-js/src/api.types.ts
index efa97d0a96..73e9fca69f 100644
--- a/packages/firefish-js/src/api.types.ts
+++ b/packages/firefish-js/src/api.types.ts
@@ -279,6 +279,12 @@ export type Endpoints = {
 		res: DriveFile[];
 	};
 	"drive/files/attached-notes": { req: TODO; res: Note[] };
+	"drive/files/caption-image": {
+		req: {
+			url: string,
+		}
+		res: string,
+	};
 	"drive/files/check-existence": { req: TODO; res: TODO };
 	"drive/files/create": { req: TODO; res: TODO };
 	"drive/files/delete": { req: { fileId: DriveFile["id"] }; res: null };

From 5da03666b2fd29193c45ec88cf4f3766987a53bc Mon Sep 17 00:00:00 2001
From: Lhcfl <Lhcfl@outlook.com>
Date: Fri, 12 Apr 2024 11:16:26 +0800
Subject: [PATCH 031/110] fix types of component

---
 biome.json                                    |  3 +-
 .../client/src/components/MkDrive.file.vue    |  7 ++--
 .../client/src/components/MkLaunchPad.vue     |  5 ++-
 .../client/src/components/MkMediaCaption.vue  |  2 +-
 .../client/src/components/MkMediaList.vue     | 32 +++++++++++--------
 packages/client/src/components/MkMention.vue  |  3 +-
 .../client/src/components/MkMenu.child.vue    |  6 ++--
 packages/client/src/components/MkMenu.vue     | 27 ++++++++++------
 .../client/src/components/MkMiniChart.vue     |  2 +-
 packages/client/src/components/MkModal.vue    | 20 ++++++++----
 .../client/src/components/MkPopupMenu.vue     |  2 +-
 packages/client/src/os.ts                     |  4 +--
 12 files changed, 68 insertions(+), 45 deletions(-)

diff --git a/biome.json b/biome.json
index 9bf08ad553..80f0d63eb4 100644
--- a/biome.json
+++ b/biome.json
@@ -21,7 +21,8 @@
 						"useImportType": "warn",
 						"useShorthandFunctionType": "warn",
 						"useTemplate": "warn",
-						"noNonNullAssertion": "off"
+						"noNonNullAssertion": "off",
+						"useNodejsImportProtocol": "off"
 					}
 				}
 			}
diff --git a/packages/client/src/components/MkDrive.file.vue b/packages/client/src/components/MkDrive.file.vue
index 9d09da663e..80a428adc3 100644
--- a/packages/client/src/components/MkDrive.file.vue
+++ b/packages/client/src/components/MkDrive.file.vue
@@ -181,12 +181,15 @@ function describe() {
 			image: props.file,
 		},
 		{
-			done: (result) => {
+			done: (result: {
+				canceled: boolean,
+				result?: string | null,
+			}) => {
 				if (!result || result.canceled) return;
 				const comment = result.result;
 				os.api("drive/files/update", {
 					fileId: props.file.id,
-					comment: comment.length === 0 ? null : comment,
+					comment: comment || null,
 				});
 			},
 		},
diff --git a/packages/client/src/components/MkLaunchPad.vue b/packages/client/src/components/MkLaunchPad.vue
index d17857dc43..084283c270 100644
--- a/packages/client/src/components/MkLaunchPad.vue
+++ b/packages/client/src/components/MkLaunchPad.vue
@@ -73,7 +73,10 @@ import { deviceKind } from "@/scripts/device-kind";
 const props = withDefaults(
 	defineProps<{
 		src?: HTMLElement;
-		anchor?: { x: string; y: string };
+		anchor?: { 
+			x: "left" | "center" | "right";
+			y: "top" | "center" | "bottom";
+		};
 	}>(),
 	{
 		anchor: () => ({ x: "right", y: "center" }),
diff --git a/packages/client/src/components/MkMediaCaption.vue b/packages/client/src/components/MkMediaCaption.vue
index 815b7c5adf..32ecceda21 100644
--- a/packages/client/src/components/MkMediaCaption.vue
+++ b/packages/client/src/components/MkMediaCaption.vue
@@ -100,7 +100,7 @@ const props = withDefaults(
 );
 
 const emit = defineEmits<{
-	done: [v: { canceled: boolean; result?: string | null }];
+	done: [result: { canceled: boolean; result?: string | null }];
 	closed: [];
 }>();
 
diff --git a/packages/client/src/components/MkMediaList.vue b/packages/client/src/components/MkMediaList.vue
index af93e24165..39f618779f 100644
--- a/packages/client/src/components/MkMediaList.vue
+++ b/packages/client/src/components/MkMediaList.vue
@@ -22,7 +22,7 @@
 							media.type.startsWith('video') ||
 							media.type.startsWith('image')
 						"
-						:key="media.id"
+						:key="`m-${media.id}`"
 						:class="{ image: media.type.startsWith('image') }"
 						:data-id="media.id"
 						:media="media"
@@ -30,7 +30,7 @@
 					/>
 					<XModPlayer
 						v-else-if="isModule(media)"
-						:key="media.id"
+						:key="`p-${media.id}`"
 						:module="media"
 					/>
 				</template>
@@ -48,7 +48,7 @@ import "photoswipe/style.css";
 import XBanner from "@/components/MkMediaBanner.vue";
 import XMedia from "@/components/MkMedia.vue";
 import XModPlayer from "@/components/MkModPlayer.vue";
-import * as os from "@/os";
+// import * as os from "@/os";
 import {
 	FILE_EXT_TRACKER_MODULES,
 	FILE_TYPE_BROWSERSAFE,
@@ -61,8 +61,8 @@ const props = defineProps<{
 	inDm?: boolean;
 }>();
 
-const gallery = ref(null);
-const pswpZIndex = os.claimZIndex("middle");
+const gallery = ref<HTMLElement | null>(null);
+// const pswpZIndex = os.claimZIndex("middle");
 
 onMounted(() => {
 	const lightbox = new PhotoSwipeLightbox({
@@ -79,7 +79,7 @@ onMounted(() => {
 					src: media.url,
 					w: media.properties.width,
 					h: media.properties.height,
-					alt: media.comment,
+					alt: media.comment || undefined,
 				};
 				if (
 					media.properties.orientation != null &&
@@ -89,7 +89,7 @@ onMounted(() => {
 				}
 				return item;
 			}),
-		gallery: gallery.value,
+		gallery: gallery.value || undefined,
 		children: ".image",
 		thumbSelector: ".image img",
 		loop: false,
@@ -119,9 +119,13 @@ onMounted(() => {
 		// element is children
 		const { element } = itemData;
 
+		if (element == null) return;
+
 		const id = element.dataset.id;
 		const file = props.mediaList.find((media) => media.id === id);
 
+		if (file == null) return;
+
 		itemData.src = file.url;
 		itemData.w = Number(file.properties.width);
 		itemData.h = Number(file.properties.height);
@@ -132,12 +136,12 @@ onMounted(() => {
 			[itemData.w, itemData.h] = [itemData.h, itemData.w];
 		}
 		itemData.msrc = file.thumbnailUrl;
-		itemData.alt = file.comment;
+		itemData.alt = file.comment || undefined;
 		itemData.thumbCropped = true;
 	});
 
 	lightbox.on("uiRegister", () => {
-		lightbox.pswp.ui.registerElement({
+		lightbox.pswp?.ui?.registerElement({
 			name: "altText",
 			className: "pwsp__alt-text-container",
 			appendTo: "wrapper",
@@ -146,7 +150,7 @@ onMounted(() => {
 				textBox.className = "pwsp__alt-text";
 				el.appendChild(textBox);
 
-				const preventProp = function (ev: Event): void {
+				const preventProp = (ev: Event): void => {
 					ev.stopPropagation();
 				};
 
@@ -158,7 +162,7 @@ onMounted(() => {
 				el.onpointermove = preventProp;
 
 				pwsp.on("change", () => {
-					textBox.textContent = pwsp.currSlide.data.alt?.trim();
+					textBox.textContent = pwsp.currSlide?.data.alt?.trim() ?? null;
 				});
 			},
 		});
@@ -168,7 +172,7 @@ onMounted(() => {
 		history.pushState(null, "", location.href);
 		addEventListener("popstate", close);
 		// This is a workaround. Not sure why, but when clicking to open, it doesn't move focus to the photoswipe. Preventing using esc to close. However when using keyboard to open it already focuses the lightbox fine.
-		lightbox.pswp.element.focus();
+		lightbox.pswp?.element?.focus();
 	});
 	lightbox.on("close", () => {
 		removeEventListener("popstate", close);
@@ -180,7 +184,7 @@ onMounted(() => {
 	function close() {
 		removeEventListener("popstate", close);
 		history.forward();
-		lightbox.pswp.close();
+		lightbox.pswp?.close();
 	}
 });
 
@@ -198,7 +202,7 @@ const isModule = (file: entities.DriveFile): boolean => {
 	return (
 		FILE_TYPE_TRACKER_MODULES.includes(file.type) ||
 		FILE_EXT_TRACKER_MODULES.some((ext) => {
-			return file.name.toLowerCase().endsWith("." + ext);
+			return file.name.toLowerCase().endsWith(`.${ext}`);
 		})
 	);
 };
diff --git a/packages/client/src/components/MkMention.vue b/packages/client/src/components/MkMention.vue
index c2c38d313e..f943997459 100644
--- a/packages/client/src/components/MkMention.vue
+++ b/packages/client/src/components/MkMention.vue
@@ -23,7 +23,6 @@
 		:href="url"
 		target="_blank"
 		rel="noopener"
-		:style="{ background: bgCss }"
 		@click.stop
 	>
 		<span class="main">
@@ -54,7 +53,7 @@ const url = `/${canonical}`;
 const isMe =
 	isSignedIn &&
 	`@${props.username}@${toUnicode(props.host)}`.toLowerCase() ===
-		`@${me.username}@${toUnicode(localHost)}`.toLowerCase();
+		`@${me!.username}@${toUnicode(localHost)}`.toLowerCase();
 </script>
 
 <style lang="scss" scoped>
diff --git a/packages/client/src/components/MkMenu.child.vue b/packages/client/src/components/MkMenu.child.vue
index 688b3ce3a4..ed788c4375 100644
--- a/packages/client/src/components/MkMenu.child.vue
+++ b/packages/client/src/components/MkMenu.child.vue
@@ -37,8 +37,8 @@ function setPosition() {
 	const rect = props.targetElement.getBoundingClientRect();
 	const left = props.targetElement.offsetWidth;
 	const top = rect.top - rootRect.top - 8;
-	el.value.style.left = left + "px";
-	el.value.style.top = top + "px";
+	el.value!.style.left = `${left}px`;
+	el.value!.style.top = `${top}px`;
 }
 
 function onChildClosed(actioned?: boolean) {
@@ -58,7 +58,7 @@ onMounted(() => {
 
 defineExpose({
 	checkHit: (ev: MouseEvent) => {
-		return ev.target === el.value || el.value.contains(ev.target);
+		return ev.target === el.value || el.value?.contains(ev.target as Node);
 	},
 });
 </script>
diff --git a/packages/client/src/components/MkMenu.vue b/packages/client/src/components/MkMenu.vue
index 2a7db40be8..c778f0f6f1 100644
--- a/packages/client/src/components/MkMenu.vue
+++ b/packages/client/src/components/MkMenu.vue
@@ -89,7 +89,8 @@
 						></span>
 					</a>
 					<button
-						v-else-if="item.type === 'user' && !items.hidden"
+						v-else-if="item.type === 'user'"
+						v-show="!item.hidden"
 						class="_button item"
 						:class="{ active: item.active }"
 						:disabled="item.active"
@@ -206,6 +207,7 @@ import {
 	onMounted,
 	ref,
 	watch,
+	shallowRef,
 } from "vue";
 import { FocusTrap } from "focus-trap-vue";
 import FormSwitch from "@/components/form/switch.vue";
@@ -213,6 +215,7 @@ import type {
 	InnerMenuItem,
 	MenuAction,
 	MenuItem,
+	MenuParent,
 	MenuPending,
 } from "@/types/menu";
 import * as os from "@/os";
@@ -234,20 +237,24 @@ const props = defineProps<{
 }>();
 
 const emit = defineEmits<{
-	(ev: "close", actioned?: boolean): void;
+	close: [actioned?: boolean];
 }>();
 
 const itemsEl = ref<HTMLDivElement>();
 
-const items2: InnerMenuItem[] = ref([]);
+const items2 = shallowRef<InnerMenuItem[]>([]);
 
 const child = ref<InstanceType<typeof XChild>>();
 
 const childShowingItem = ref<MenuItem | null>();
 
+// FIXME: this is not used
+const isActive = ref();
+
 watch(
 	() => props.items,
 	() => {
+		// FIXME: what's this?
 		const items: (MenuItem | MenuPending)[] = [...props.items].filter(
 			(item) => item !== undefined,
 		);
@@ -288,29 +295,29 @@ function onGlobalMousedown(event: MouseEvent) {
 	if (
 		childTarget.value &&
 		(event.target === childTarget.value ||
-			childTarget.value.contains(event.target))
+			childTarget.value.contains(event.target as Node))
 	)
 		return;
-	if (child.value && child.value.checkHit(event)) return;
+	if (child.value?.checkHit(event)) return;
 	closeChild();
 }
 
 let childCloseTimer: null | number = null;
-function onItemMouseEnter(item) {
+function onItemMouseEnter(_item) {
 	childCloseTimer = window.setTimeout(() => {
 		closeChild();
 	}, 300);
 }
-function onItemMouseLeave(item) {
+function onItemMouseLeave(_item) {
 	if (childCloseTimer) window.clearTimeout(childCloseTimer);
 }
 
-async function showChildren(item: MenuItem, ev: MouseEvent) {
+async function showChildren(item: MenuParent, ev: MouseEvent) {
 	if (props.asDrawer) {
-		os.popupMenu(item.children, ev.currentTarget ?? ev.target);
+		os.popupMenu(item.children, (ev.currentTarget ?? ev.target) as HTMLElement);
 		close();
 	} else {
-		childTarget.value = ev.currentTarget ?? ev.target;
+		childTarget.value = (ev.currentTarget ?? ev.target) as HTMLElement;
 		childMenu.value = item.children;
 		childShowingItem.value = item;
 	}
diff --git a/packages/client/src/components/MkMiniChart.vue b/packages/client/src/components/MkMiniChart.vue
index bfa83f3d80..e82733c715 100644
--- a/packages/client/src/components/MkMiniChart.vue
+++ b/packages/client/src/components/MkMiniChart.vue
@@ -20,7 +20,7 @@
 			:stroke="color"
 			stroke-width="2"
 		/>
-		<circle :cx="headX" :cy="headY" r="3" :fill="color" />
+		<circle :cx="headX ?? undefined" :cy="headY ?? undefined" r="3" :fill="color" />
 	</svg>
 </template>
 
diff --git a/packages/client/src/components/MkModal.vue b/packages/client/src/components/MkModal.vue
index 6a2897cebf..57ecc1b0d8 100644
--- a/packages/client/src/components/MkModal.vue
+++ b/packages/client/src/components/MkModal.vue
@@ -108,8 +108,11 @@ type ModalTypes = "popup" | "dialog" | "dialog:top" | "drawer";
 const props = withDefaults(
 	defineProps<{
 		manualShowing?: boolean | null;
-		anchor?: { x: string; y: string };
-		src?: HTMLElement;
+		anchor?: { 
+			x: "left" | "center" | "right";
+			y: "top" | "center" | "bottom";
+		};
+		src?: HTMLElement | null;
 		preferType?: ModalTypes | "auto";
 		zPriority?: "low" | "middle" | "high";
 		noOverlap?: boolean;
@@ -118,7 +121,7 @@ const props = withDefaults(
 	}>(),
 	{
 		manualShowing: null,
-		src: undefined,
+		src: null,
 		anchor: () => ({ x: "center", y: "bottom" }),
 		preferType: "auto",
 		zPriority: "low",
@@ -139,6 +142,9 @@ const emit = defineEmits<{
 
 provide("modal", true);
 
+// FIXME: this may not used
+const isActive = ref();
+
 const maxHeight = ref<number>();
 const fixed = ref(false);
 const transformOrigin = ref("center");
@@ -189,7 +195,7 @@ const transitionDuration = computed(() =>
 
 let contentClicking = false;
 
-const focusedElement = document.activeElement;
+const focusedElement = document.activeElement as HTMLElement;
 function close(_ev?, opts: { useSendAnimation?: boolean } = {}) {
 	// removeEventListener("popstate", close);
 	// if (props.preferType == "dialog") {
@@ -204,7 +210,7 @@ function close(_ev?, opts: { useSendAnimation?: boolean } = {}) {
 	showing.value = false;
 	emit("close");
 	if (!props.noReturnFocus) {
-		focusedElement.focus();
+		focusedElement?.focus();
 	}
 }
 
@@ -235,8 +241,8 @@ const align = () => {
 	const width = content.value!.offsetWidth;
 	const height = content.value!.offsetHeight;
 
-	let left: number;
-	let top: number;
+	let left = 0;
+	let top = MARGIN;
 
 	const x = srcRect.left + (fixed.value ? 0 : window.scrollX);
 	const y = srcRect.top + (fixed.value ? 0 : window.scrollY);
diff --git a/packages/client/src/components/MkPopupMenu.vue b/packages/client/src/components/MkPopupMenu.vue
index 9a98648ea2..1bee69a0b2 100644
--- a/packages/client/src/components/MkPopupMenu.vue
+++ b/packages/client/src/components/MkPopupMenu.vue
@@ -35,7 +35,7 @@ defineProps<{
 	align?: "center" | string;
 	width?: number;
 	viaKeyboard?: boolean;
-	src?: any;
+	src?: HTMLElement | null;
 	noReturnFocus?;
 }>();
 
diff --git a/packages/client/src/os.ts b/packages/client/src/os.ts
index 426fd29084..32d84396ef 100644
--- a/packages/client/src/os.ts
+++ b/packages/client/src/os.ts
@@ -213,7 +213,7 @@ interface VueComponentConstructor<P, E> {
 export async function popup<Props, Emits>(
 	component: VueComponentConstructor<Props, Emits>,
 	props: Props & Record<string, unknown>,
-	events: Partial<NonNullable<Emits>> | Record<string, never> = {},
+	events: Partial<Emits> = {},
 	disposeEvent?: string,
 ) {
 	markRaw(component);
@@ -858,7 +858,7 @@ export async function openEmojiPicker(
 
 export function popupMenu(
 	items: MenuItem[] | Ref<MenuItem[]>,
-	src?: HTMLElement,
+	src?: HTMLElement | null,
 	options?: {
 		align?: string;
 		width?: number;

From 2bf51abc126e01db2a90fb1c544016b639666b31 Mon Sep 17 00:00:00 2001
From: Lhcfl <Lhcfl@outlook.com>
Date: Fri, 12 Apr 2024 11:31:11 +0800
Subject: [PATCH 032/110] fix type of MkModalPageWindow

---
 .../client/src/components/MkDrive.file.vue    |  4 ++--
 .../client/src/components/MkLaunchPad.vue     |  2 +-
 packages/client/src/components/MkModal.vue    |  2 +-
 .../src/components/MkModalPageWindow.vue      | 22 +++++++++++--------
 packages/client/src/nirax.ts                  |  2 +-
 packages/client/src/os.ts                     | 17 +++++++-------
 packages/client/src/router.ts                 |  6 ++---
 packages/client/src/scripts/page-metadata.ts  |  1 +
 packages/firefish-js/src/api.types.ts         |  6 ++---
 9 files changed, 33 insertions(+), 29 deletions(-)

diff --git a/packages/client/src/components/MkDrive.file.vue b/packages/client/src/components/MkDrive.file.vue
index 80a428adc3..6d348a33e7 100644
--- a/packages/client/src/components/MkDrive.file.vue
+++ b/packages/client/src/components/MkDrive.file.vue
@@ -182,8 +182,8 @@ function describe() {
 		},
 		{
 			done: (result: {
-				canceled: boolean,
-				result?: string | null,
+				canceled: boolean;
+				result?: string | null;
 			}) => {
 				if (!result || result.canceled) return;
 				const comment = result.result;
diff --git a/packages/client/src/components/MkLaunchPad.vue b/packages/client/src/components/MkLaunchPad.vue
index 084283c270..b3ca7bd34d 100644
--- a/packages/client/src/components/MkLaunchPad.vue
+++ b/packages/client/src/components/MkLaunchPad.vue
@@ -73,7 +73,7 @@ import { deviceKind } from "@/scripts/device-kind";
 const props = withDefaults(
 	defineProps<{
 		src?: HTMLElement;
-		anchor?: { 
+		anchor?: {
 			x: "left" | "center" | "right";
 			y: "top" | "center" | "bottom";
 		};
diff --git a/packages/client/src/components/MkModal.vue b/packages/client/src/components/MkModal.vue
index 57ecc1b0d8..2bed9f8295 100644
--- a/packages/client/src/components/MkModal.vue
+++ b/packages/client/src/components/MkModal.vue
@@ -108,7 +108,7 @@ type ModalTypes = "popup" | "dialog" | "dialog:top" | "drawer";
 const props = withDefaults(
 	defineProps<{
 		manualShowing?: boolean | null;
-		anchor?: { 
+		anchor?: {
 			x: "left" | "center" | "right";
 			y: "top" | "center" | "bottom";
 		};
diff --git a/packages/client/src/components/MkModalPageWindow.vue b/packages/client/src/components/MkModalPageWindow.vue
index e9c8136ae4..a4dea9ee32 100644
--- a/packages/client/src/components/MkModalPageWindow.vue
+++ b/packages/client/src/components/MkModalPageWindow.vue
@@ -29,7 +29,7 @@
 				<button
 					class="_button"
 					:aria-label="i18n.ts.close"
-					@click="$refs.modal.close()"
+					@click="modal!.close()"
 				>
 					<i :class="icon('ph-x')"></i>
 				</button>
@@ -65,6 +65,7 @@ import type { PageMetadata } from "@/scripts/page-metadata";
 import { provideMetadataReceiver } from "@/scripts/page-metadata";
 import { Router } from "@/nirax";
 import icon from "@/scripts/icon";
+import type { MenuItem } from "@/types/menu";
 
 const props = defineProps<{
 	initialPath: string;
@@ -81,11 +82,11 @@ router.addListener("push", (ctx) => {});
 
 const pageMetadata = ref<null | ComputedRef<PageMetadata>>();
 const rootEl = ref();
-const modal = ref<InstanceType<typeof MkModal>>();
+const modal = ref<InstanceType<typeof MkModal> | null>(null);
 const path = ref(props.initialPath);
 const width = ref(860);
 const height = ref(660);
-const history = [];
+const history: string[] = [];
 
 provide("router", router);
 provideMetadataReceiver((info) => {
@@ -95,7 +96,7 @@ provide("shouldOmitHeaderTitle", true);
 provide("shouldHeaderThin", true);
 
 const pageUrl = computed(() => url + path.value);
-const contextmenu = computed(() => {
+const contextmenu = computed((): MenuItem[] => {
 	return [
 		{
 			type: "label",
@@ -117,7 +118,7 @@ const contextmenu = computed(() => {
 			text: i18n.ts.openInNewTab,
 			action: () => {
 				window.open(pageUrl.value, "_blank");
-				modal.value.close();
+				modal.value!.close();
 			},
 		},
 		{
@@ -130,23 +131,26 @@ const contextmenu = computed(() => {
 	];
 });
 
-function navigate(path, record = true) {
+function navigate(path: string, record = true) {
 	if (record) history.push(router.getCurrentPath());
 	router.push(path);
 }
 
 function back() {
-	navigate(history.pop(), false);
+	const backTo = history.pop();
+	if (backTo) {
+		navigate(backTo, false);
+	}
 }
 
 function expand() {
 	mainRouter.push(path.value);
-	modal.value.close();
+	modal.value!.close();
 }
 
 function popout() {
 	_popout(path.value, rootEl.value);
-	modal.value.close();
+	modal.value!.close();
 }
 
 function onContextmenu(ev: MouseEvent) {
diff --git a/packages/client/src/nirax.ts b/packages/client/src/nirax.ts
index f214060762..da162338b6 100644
--- a/packages/client/src/nirax.ts
+++ b/packages/client/src/nirax.ts
@@ -6,7 +6,7 @@ import { shallowRef } from "vue";
 import { safeURIDecode } from "@/scripts/safe-uri-decode";
 import { pleaseLogin } from "@/scripts/please-login";
 
-interface RouteDef {
+export interface RouteDef {
 	path: string;
 	component: Component;
 	query?: Record<string, string>;
diff --git a/packages/client/src/os.ts b/packages/client/src/os.ts
index 32d84396ef..49d03b76cc 100644
--- a/packages/client/src/os.ts
+++ b/packages/client/src/os.ts
@@ -3,10 +3,7 @@
 import { EventEmitter } from "eventemitter3";
 import { type entities, api as firefishApi } from "firefish-js";
 import insertTextAtCursor from "insert-text-at-cursor";
-import type {
-	Component,
-	Ref,
-} from "vue";
+import type { Component, Ref } from "vue";
 import { defineAsyncComponent, markRaw, ref } from "vue";
 import { i18n } from "./i18n";
 import MkDialog from "@/components/MkDialog.vue";
@@ -177,11 +174,13 @@ export function promiseDialog<T>(
 }
 
 let popupIdCount = 0;
-export const popups = ref<{
-	id: number;
-	component: Component;
-	props: Record<string, unknown>;
-}[]>([]);
+export const popups = ref<
+	{
+		id: number;
+		component: Component;
+		props: Record<string, unknown>;
+	}[]
+>([]);
 
 const zIndexes = {
 	low: 1000000,
diff --git a/packages/client/src/router.ts b/packages/client/src/router.ts
index a21df2981e..7273919261 100644
--- a/packages/client/src/router.ts
+++ b/packages/client/src/router.ts
@@ -1,18 +1,18 @@
 import type { AsyncComponentLoader } from "vue";
 import { defineAsyncComponent, inject } from "vue";
 import { isEmojiMod, isModerator, me } from "@/me";
-import { Router } from "@/nirax";
+import { type RouteDef, Router } from "@/nirax";
 import MkError from "@/pages/_error_.vue";
 import MkLoading from "@/pages/_loading_.vue";
 
-const page = (loader: AsyncComponentLoader<any>) =>
+const page = (loader: AsyncComponentLoader) =>
 	defineAsyncComponent({
 		loader,
 		loadingComponent: MkLoading,
 		errorComponent: MkError,
 	});
 
-export const routes = [
+export const routes: RouteDef[] = [
 	{
 		path: "/@:initUser/pages/:initPageName/view-source",
 		component: page(() => import("./pages/page-editor/page-editor.vue")),
diff --git a/packages/client/src/scripts/page-metadata.ts b/packages/client/src/scripts/page-metadata.ts
index e2f470d0bb..cf8d7938a7 100644
--- a/packages/client/src/scripts/page-metadata.ts
+++ b/packages/client/src/scripts/page-metadata.ts
@@ -12,6 +12,7 @@ export interface PageMetadata {
 	avatar?: entities.UserDetailed | null;
 	userName?: entities.User | null;
 	bg?: string;
+	hideHeader?: boolean;
 }
 
 export function definePageMetadata(
diff --git a/packages/firefish-js/src/api.types.ts b/packages/firefish-js/src/api.types.ts
index 73e9fca69f..890e6bcd48 100644
--- a/packages/firefish-js/src/api.types.ts
+++ b/packages/firefish-js/src/api.types.ts
@@ -281,9 +281,9 @@ export type Endpoints = {
 	"drive/files/attached-notes": { req: TODO; res: Note[] };
 	"drive/files/caption-image": {
 		req: {
-			url: string,
-		}
-		res: string,
+			url: string;
+		};
+		res: string;
 	};
 	"drive/files/check-existence": { req: TODO; res: TODO };
 	"drive/files/create": { req: TODO; res: TODO };

From ea6ef881c21bb54297a02b3d3e013a44dd2330a7 Mon Sep 17 00:00:00 2001
From: Lhcfl <Lhcfl@outlook.com>
Date: Fri, 12 Apr 2024 14:08:20 +0800
Subject: [PATCH 033/110] fix types of components

---
 packages/client/src/components/MkModPlayer.vue   | 7 ++++---
 packages/client/src/components/MkModalWindow.vue | 7 +++++--
 2 files changed, 9 insertions(+), 5 deletions(-)

diff --git a/packages/client/src/components/MkModPlayer.vue b/packages/client/src/components/MkModPlayer.vue
index cdaa5f7040..21eecbdf6d 100644
--- a/packages/client/src/components/MkModPlayer.vue
+++ b/packages/client/src/components/MkModPlayer.vue
@@ -140,7 +140,7 @@ const patternShow = ref(false);
 const modPattern = ref<HTMLDivElement>();
 const progress = ref<typeof FormRange>();
 const position = ref(0);
-const patData = shallowRef([] as ModRow[][]);
+const patData = shallowRef<readonly ModRow[][]>([]);
 const currentPattern = ref(0);
 const nbChannels = ref(0);
 const length = ref(1);
@@ -159,7 +159,7 @@ function load() {
 			error.value = false;
 			fetching.value = false;
 		})
-		.catch((e: any) => {
+		.catch((e: unknown) => {
 			console.error(e);
 			error.value = true;
 			fetching.value = false;
@@ -293,12 +293,13 @@ function isRowActive(i: number) {
 		}
 		return true;
 	}
+	return false;
 }
 
 function indexText(i: number) {
 	let rowText = i.toString(16);
 	if (rowText.length === 1) {
-		rowText = "0" + rowText;
+		rowText = `0${rowText}`;
 	}
 	return rowText;
 }
diff --git a/packages/client/src/components/MkModalWindow.vue b/packages/client/src/components/MkModalWindow.vue
index c9d126ee90..6471bf1722 100644
--- a/packages/client/src/components/MkModalWindow.vue
+++ b/packages/client/src/components/MkModalWindow.vue
@@ -15,7 +15,7 @@
 					height: scroll
 						? height
 							? `${props.height}px`
-							: null
+							: undefined
 						: height
 							? `min(${props.height}px, 100%)`
 							: '100%',
@@ -65,7 +65,7 @@
 </template>
 
 <script lang="ts" setup>
-import { shallowRef } from "vue";
+import { ref, shallowRef } from "vue";
 
 import { FocusTrap } from "focus-trap-vue";
 import MkModal from "./MkModal.vue";
@@ -96,6 +96,9 @@ const emit = defineEmits<{
 	(event: "ok"): void;
 }>();
 
+// FIXME: seems that this is not used
+const isActive = ref();
+
 const modal = shallowRef<InstanceType<typeof MkModal>>();
 const rootEl = shallowRef<HTMLElement>();
 const headerEl = shallowRef<HTMLElement>();

From e4927c9b9bff924a7eefd48b920d9341757a9a00 Mon Sep 17 00:00:00 2001
From: Lhcfl <Lhcfl@outlook.com>
Date: Fri, 12 Apr 2024 16:37:32 +0800
Subject: [PATCH 034/110] fix types of components

---
 .../src/components/MkInstanceTicker.vue       |  9 +--
 packages/client/src/components/MkNote.vue     | 62 +++++++++--------
 .../client/src/components/MkNoteDetailed.vue  | 66 ++++++++++---------
 .../client/src/components/MkNotePreview.vue   |  4 +-
 packages/client/src/components/MkNotes.vue    | 14 +++-
 .../MkNotificationSettingWindow.vue           |  6 +-
 .../src/components/MkNotificationToast.vue    |  3 +-
 .../client/src/components/MkNotifications.vue |  6 +-
 .../client/src/components/MkPagination.vue    | 26 +++++++-
 .../src/components/MkPostFormDialog.vue       |  6 +-
 .../client/src/components/MkRenoteButton.vue  |  6 +-
 .../src/components/MkSubNoteContent.vue       |  8 +--
 packages/client/src/components/MkUserList.vue | 15 +++--
 .../components/MkUserSelectLocalDialog.vue    | 10 +--
 .../global/MkMisskeyFlavoredMarkdown.vue      |  2 +-
 packages/client/src/components/mfm.ts         |  2 +-
 packages/client/src/config.ts                 |  2 +-
 packages/client/src/os.ts                     | 32 +++++----
 packages/client/src/pages/about.vue           | 11 ++--
 packages/client/src/pages/admin/abuses.vue    |  6 +-
 packages/client/src/pages/admin/emojis.vue    |  7 +-
 packages/client/src/pages/admin/users.vue     |  8 ++-
 packages/client/src/pages/announcements.vue   |  3 +-
 packages/client/src/pages/favorites.vue       |  5 +-
 .../client/src/pages/follow-requests-sent.vue |  7 +-
 packages/client/src/pages/follow-requests.vue |  9 ++-
 .../src/pages/messaging/messaging-room.vue    |  6 +-
 .../client/src/pages/my-antennas/index.vue    |  4 +-
 packages/client/src/pages/my-clips/index.vue  |  8 ++-
 packages/client/src/pages/my-lists/index.vue  |  8 ++-
 packages/client/src/pages/note-history.vue    |  8 ++-
 packages/client/src/pages/search.vue          |  5 +-
 packages/client/src/scripts/get-note-menu.ts  |  5 +-
 .../client/src/scripts/use-note-capture.ts    |  2 +-
 packages/client/src/store.ts                  | 24 +++++--
 packages/client/src/types/note.ts             |  5 ++
 packages/firefish-js/src/api.types.ts         | 50 +++++++++++++-
 packages/firefish-js/src/entities.ts          | 18 ++---
 38 files changed, 319 insertions(+), 159 deletions(-)

diff --git a/packages/client/src/components/MkInstanceTicker.vue b/packages/client/src/components/MkInstanceTicker.vue
index 498303ef8e..bbf999c183 100644
--- a/packages/client/src/components/MkInstanceTicker.vue
+++ b/packages/client/src/components/MkInstanceTicker.vue
@@ -20,15 +20,10 @@ import { ref } from "vue";
 import { instanceName, version } from "@/config";
 import { instance as Instance } from "@/instance";
 import { getProxiedImageUrlNullable } from "@/scripts/media-proxy";
+import type { entities } from "firefish-js";
 
 const props = defineProps<{
-	instance?: {
-		faviconUrl?: string;
-		name: string;
-		themeColor?: string;
-		softwareName?: string;
-		softwareVersion?: string;
-	};
+	instance?: entities.InstanceLite;
 }>();
 
 const ticker = ref<HTMLElement | null>(null);
diff --git a/packages/client/src/components/MkNote.vue b/packages/client/src/components/MkNote.vue
index 65dbd17d80..6a22652f60 100644
--- a/packages/client/src/components/MkNote.vue
+++ b/packages/client/src/components/MkNote.vue
@@ -9,7 +9,7 @@
 		v-vibrate="5"
 		:aria-label="accessibleLabel"
 		class="tkcbzcuz note-container"
-		:tabindex="!isDeleted ? '-1' : null"
+		:tabindex="!isDeleted ? '-1' : undefined"
 		:class="{ renote: isRenote }"
 	>
 		<MkNoteSub
@@ -112,9 +112,9 @@
 						:note="appearNote"
 						:detailed="true"
 						:detailed-view="detailedView"
-						:parent-id="appearNote.parentId"
+						:parent-id="appearNote.id"
 						@push="(e) => router.push(notePage(e))"
-						@focusfooter="footerEl.focus()"
+						@focusfooter="footerEl!.focus()"
 						@expanded="(e) => setPostExpanded(e)"
 					></MkSubNoteContent>
 					<div v-if="translating || translation" class="translation">
@@ -312,11 +312,17 @@ import { notePage } from "@/filters/note";
 import { deepClone } from "@/scripts/clone";
 import { getNoteSummary } from "@/scripts/get-note-summary";
 import icon from "@/scripts/icon";
+import type { NoteTranslation } from "@/types/note";
 
 const router = useRouter();
 
+type NoteType = entities.Note & {
+	_featuredId_?: string;
+	_prId_?: string;
+};
+
 const props = defineProps<{
-	note: entities.Note;
+	note: NoteType;
 	pinned?: boolean;
 	detailedView?: boolean;
 	collapsedReply?: boolean;
@@ -354,18 +360,18 @@ const isRenote =
 	note.value.fileIds.length === 0 &&
 	note.value.poll == null;
 
-const el = ref<HTMLElement>();
+const el = ref<HTMLElement | null>(null);
 const footerEl = ref<HTMLElement>();
 const menuButton = ref<HTMLElement>();
 const starButton = ref<InstanceType<typeof XStarButton>>();
-const renoteButton = ref<InstanceType<typeof XRenoteButton>>();
+const renoteButton = ref<InstanceType<typeof XRenoteButton> | null>(null);
 const renoteTime = ref<HTMLElement>();
-const reactButton = ref<HTMLElement>();
+const reactButton = ref<HTMLElement | null>(null);
 const appearNote = computed(() =>
-	isRenote ? (note.value.renote as entities.Note) : note.value,
+	isRenote ? (note.value.renote as NoteType) : note.value,
 );
-const isMyRenote = isSignedIn && me.id === note.value.userId;
-const showContent = ref(false);
+const isMyRenote = isSignedIn && me!.id === note.value.userId;
+// const showContent = ref(false);
 const isDeleted = ref(false);
 const muted = ref(
 	getWordSoftMute(
@@ -375,7 +381,7 @@ const muted = ref(
 		defaultStore.state.mutedLangs,
 	),
 );
-const translation = ref(null);
+const translation = ref<NoteTranslation | null>(null);
 const translating = ref(false);
 const enableEmojiReactions = defaultStore.state.enableEmojiReactions;
 const expandOnNoteClick = defaultStore.state.expandOnNoteClick;
@@ -391,7 +397,7 @@ const isForeignLanguage: boolean =
 		return postLang !== "" && postLang !== targetLang;
 	})();
 
-async function translate_(noteId, targetLang: string) {
+async function translate_(noteId: string, targetLang: string) {
 	return await os.api("notes/translate", {
 		noteId,
 		targetLang,
@@ -421,12 +427,13 @@ async function translate() {
 const keymap = {
 	r: () => reply(true),
 	"e|a|plus": () => react(true),
-	q: () => renoteButton.value.renote(true),
+	q: () => renoteButton.value!.renote(true),
 	"up|k": focusBefore,
 	"down|j": focusAfter,
 	esc: blur,
 	"m|o": () => menu(true),
-	s: () => showContent.value !== showContent.value,
+	// FIXME: What's this?
+	// s: () => showContent.value !== showContent.value,
 };
 
 if (appearNote.value.historyId == null) {
@@ -437,12 +444,12 @@ if (appearNote.value.historyId == null) {
 	});
 }
 
-function reply(viaKeyboard = false): void {
+function reply(_viaKeyboard = false): void {
 	pleaseLogin();
 	os.post(
 		{
 			reply: appearNote.value,
-			animation: !viaKeyboard,
+			// animation: !viaKeyboard,
 		},
 		() => {
 			focus();
@@ -450,11 +457,11 @@ function reply(viaKeyboard = false): void {
 	);
 }
 
-function react(viaKeyboard = false): void {
+function react(_viaKeyboard = false): void {
 	pleaseLogin();
 	blur();
 	reactionPicker.show(
-		reactButton.value,
+		reactButton.value!,
 		(reaction) => {
 			os.api("notes/reactions/create", {
 				noteId: appearNote.value.id,
@@ -467,7 +474,7 @@ function react(viaKeyboard = false): void {
 	);
 }
 
-function undoReact(note): void {
+function undoReact(note: NoteType): void {
 	const oldReaction = note.myReaction;
 	if (!oldReaction) return;
 	os.api("notes/reactions/delete", {
@@ -481,16 +488,17 @@ const currentClipPage = inject<Ref<entities.Clip> | null>(
 );
 
 function onContextmenu(ev: MouseEvent): void {
-	const isLink = (el: HTMLElement) => {
+	const isLink = (el: HTMLElement): boolean => {
 		if (el.tagName === "A") return true;
 		// The Audio element's context menu is the browser default, such as for selecting playback speed.
 		if (el.tagName === "AUDIO") return true;
 		if (el.parentElement) {
 			return isLink(el.parentElement);
 		}
+		return false;
 	};
-	if (isLink(ev.target)) return;
-	if (window.getSelection().toString() !== "") return;
+	if (isLink(ev.target as HTMLElement)) return;
+	if (window.getSelection()?.toString() !== "") return;
 
 	if (defaultStore.state.useReactionPickerForContextMenu) {
 		ev.preventDefault();
@@ -509,7 +517,7 @@ function onContextmenu(ev: MouseEvent): void {
 						os.pageWindow(notePage(appearNote.value));
 					},
 				},
-				notePage(appearNote.value) != location.pathname
+				notePage(appearNote.value) !== location.pathname
 					? {
 							icon: `${icon("ph-arrows-out-simple")}`,
 							text: i18n.ts.showInPage,
@@ -589,11 +597,11 @@ function showRenoteMenu(viaKeyboard = false): void {
 }
 
 function focus() {
-	el.value.focus();
+	el.value!.focus();
 }
 
 function blur() {
-	el.value.blur();
+	el.value!.blur();
 }
 
 function focusBefore() {
@@ -605,12 +613,12 @@ function focusAfter() {
 }
 
 function scrollIntoView() {
-	el.value.scrollIntoView();
+	el.value!.scrollIntoView();
 }
 
 function noteClick(e) {
 	if (
-		document.getSelection().type === "Range" ||
+		document.getSelection()?.type === "Range" ||
 		props.detailedView ||
 		!expandOnNoteClick
 	) {
diff --git a/packages/client/src/components/MkNoteDetailed.vue b/packages/client/src/components/MkNoteDetailed.vue
index d17e21b2b1..b6c0d784ba 100644
--- a/packages/client/src/components/MkNoteDetailed.vue
+++ b/packages/client/src/components/MkNoteDetailed.vue
@@ -6,7 +6,7 @@
 		v-hotkey="keymap"
 		v-size="{ max: [500, 350, 300] }"
 		class="lxwezrsl _block"
-		:tabindex="!isDeleted ? '-1' : null"
+		:tabindex="!isDeleted ? '-1' : undefined"
 		:class="{ renote: isRenote }"
 	>
 		<MkNoteSub
@@ -64,7 +64,7 @@
 					)
 				}}
 			</option>
-			<option v-if="directQuotes?.length > 0" value="quotes">
+			<option v-if="directQuotes && directQuotes.length > 0" value="quotes">
 				<!-- <i :class="icon('ph-quotes')"></i> -->
 				{{
 					wordWithCount(
@@ -102,7 +102,7 @@
 			:detailed-view="true"
 			:parent-id="note.id"
 		/>
-		<MkLoading v-else-if="tab === 'quotes' && directQuotes.length > 0" />
+		<MkLoading v-else-if="tab === 'quotes' && directQuotes && directQuotes.length > 0" />
 
 		<!-- <MkPagination
 			v-if="tab === 'renotes'"
@@ -225,12 +225,12 @@ if (noteViewInterruptors.length > 0) {
 	});
 }
 
-const el = ref<HTMLElement>();
+const el = ref<HTMLElement | null>(null);
 const noteEl = ref();
 const menuButton = ref<HTMLElement>();
 const renoteButton = ref<InstanceType<typeof XRenoteButton>>();
 const reactButton = ref<HTMLElement>();
-const showContent = ref(false);
+// const showContent = ref(false);
 const isDeleted = ref(false);
 const muted = ref(
 	getWordSoftMute(
@@ -248,7 +248,8 @@ const directReplies = ref<null | entities.Note[]>([]);
 const directQuotes = ref<null | entities.Note[]>([]);
 const clips = ref();
 const renotes = ref();
-let isScrolling;
+const isRenote = ref(note.value.renoteId != null);
+let isScrolling: boolean;
 
 const reactionsCount = Object.values(props.note.reactions).reduce(
 	(x, y) => x + y,
@@ -258,10 +259,10 @@ const reactionsCount = Object.values(props.note.reactions).reduce(
 const keymap = {
 	r: () => reply(true),
 	"e|a|plus": () => react(true),
-	q: () => renoteButton.value.renote(true),
+	q: () => renoteButton.value!.renote(true),
 	esc: blur,
 	"m|o": () => menu(true),
-	s: () => showContent.value !== showContent.value,
+	// s: () => showContent.value !== showContent.value,
 };
 
 useNoteCapture({
@@ -270,21 +271,21 @@ useNoteCapture({
 	isDeletedRef: isDeleted,
 });
 
-function reply(viaKeyboard = false): void {
+function reply(_viaKeyboard = false): void {
 	pleaseLogin();
 	os.post({
 		reply: note.value,
-		animation: !viaKeyboard,
+		// animation: !viaKeyboard,
 	}).then(() => {
 		focus();
 	});
 }
 
-function react(viaKeyboard = false): void {
+function react(_viaKeyboard = false): void {
 	pleaseLogin();
 	blur();
 	reactionPicker.show(
-		reactButton.value,
+		reactButton.value!,
 		(reaction) => {
 			os.api("notes/reactions/create", {
 				noteId: note.value.id,
@@ -297,13 +298,13 @@ function react(viaKeyboard = false): void {
 	);
 }
 
-function undoReact(note): void {
-	const oldReaction = note.myReaction;
-	if (!oldReaction) return;
-	os.api("notes/reactions/delete", {
-		noteId: note.id,
-	});
-}
+// function undoReact(note): void {
+// 	const oldReaction = note.myReaction;
+// 	if (!oldReaction) return;
+// 	os.api("notes/reactions/delete", {
+// 		noteId: note.id,
+// 	});
+// }
 
 function onContextmenu(ev: MouseEvent): void {
 	const isLink = (el: HTMLElement) => {
@@ -312,8 +313,8 @@ function onContextmenu(ev: MouseEvent): void {
 			return isLink(el.parentElement);
 		}
 	};
-	if (isLink(ev.target)) return;
-	if (window.getSelection().toString() !== "") return;
+	if (isLink(ev.target as HTMLElement)) return;
+	if (window.getSelection()?.toString() !== "") return;
 
 	if (defaultStore.state.useReactionPickerForContextMenu) {
 		ev.preventDefault();
@@ -362,12 +363,17 @@ os.api("notes/children", {
 	limit: 30,
 	depth: 12,
 }).then((res) => {
-	res = res.reduce((acc, resNote) => {
-		if (resNote.userId == note.value.userId) {
-			return [...acc, resNote];
-		}
-		return [resNote, ...acc];
-	}, []);
+	// biome-ignore lint/style/noParameterAssign: assign it intentially
+	res = res
+		.filter((n) => n.userId !== note.value.userId)
+		.reverse()
+		.concat(res.filter((n) => n.userId === note.value.userId));
+	// res = res.reduce((acc: entities.Note[], resNote) => {
+	// 	if (resNote.userId === note.value.userId) {
+	// 		return [...acc, resNote];
+	// 	}
+	// 	return [resNote, ...acc];
+	// }, []);
 	replies.value = res;
 	directReplies.value = res
 		.filter((resNote) => resNote.replyId === note.value.id)
@@ -438,7 +444,7 @@ async function onNoteUpdated(
 	}
 
 	switch (type) {
-		case "replied":
+		case "replied": {
 			const { id: createdId } = body;
 			const replyNote = await os.api("notes/show", {
 				noteId: createdId,
@@ -446,10 +452,10 @@ async function onNoteUpdated(
 
 			replies.value.splice(found, 0, replyNote);
 			if (found === 0) {
-				directReplies.value.push(replyNote);
+				directReplies.value!.push(replyNote);
 			}
 			break;
-
+		}
 		case "deleted":
 			if (found === 0) {
 				isDeleted.value = true;
diff --git a/packages/client/src/components/MkNotePreview.vue b/packages/client/src/components/MkNotePreview.vue
index 454936dfbe..9ba8c0025e 100644
--- a/packages/client/src/components/MkNotePreview.vue
+++ b/packages/client/src/components/MkNotePreview.vue
@@ -1,9 +1,9 @@
 <template>
 	<div v-size="{ min: [350, 500] }" class="fefdfafb">
-		<MkAvatar class="avatar" :user="me" disable-link />
+		<MkAvatar class="avatar" :user="me!" disable-link />
 		<div class="main">
 			<div class="header">
-				<MkUserName :user="me" />
+				<MkUserName :user="me!" />
 			</div>
 			<div class="body">
 				<div class="content">
diff --git a/packages/client/src/components/MkNotes.vue b/packages/client/src/components/MkNotes.vue
index 2351cd6e47..b5fdc95fbf 100644
--- a/packages/client/src/components/MkNotes.vue
+++ b/packages/client/src/components/MkNotes.vue
@@ -40,7 +40,11 @@
 
 <script lang="ts" setup>
 import { ref } from "vue";
-import type { PagingOf } from "@/components/MkPagination.vue";
+import type {
+	MkPaginationType,
+	PagingKeyOf,
+	PagingOf,
+} from "@/components/MkPagination.vue";
 import XNote from "@/components/MkNote.vue";
 import XList from "@/components/MkDateSeparatedList.vue";
 import MkPagination from "@/components/MkPagination.vue";
@@ -56,10 +60,14 @@ defineProps<{
 	disableAutoLoad?: boolean;
 }>();
 
-const pagingComponent = ref<InstanceType<typeof MkPagination>>();
+const pagingComponent = ref<MkPaginationType<
+	PagingKeyOf<entities.Note>
+> | null>(null);
 
 function scrollTop() {
-	scroll(tlEl.value, { top: 0, behavior: "smooth" });
+	if (tlEl.value) {
+		scroll(tlEl.value, { top: 0, behavior: "smooth" });
+	}
 }
 
 defineExpose({
diff --git a/packages/client/src/components/MkNotificationSettingWindow.vue b/packages/client/src/components/MkNotificationSettingWindow.vue
index fac382f68c..20c3b43cb3 100644
--- a/packages/client/src/components/MkNotificationSettingWindow.vue
+++ b/packages/client/src/components/MkNotificationSettingWindow.vue
@@ -6,7 +6,7 @@
 		:with-ok-button="true"
 		:ok-button-disabled="false"
 		@ok="ok()"
-		@close="dialog.close()"
+		@close="dialog!.close()"
 		@closed="emit('closed')"
 	>
 		<template #header>{{ i18n.ts.notificationSetting }}</template>
@@ -68,7 +68,7 @@ const includingTypes = computed(() => props.includingTypes || []);
 
 const dialog = ref<InstanceType<typeof XModalWindow>>();
 
-const typesMap = ref<Record<(typeof notificationTypes)[number], boolean>>({});
+const typesMap = ref({} as Record<(typeof notificationTypes)[number], boolean>);
 const useGlobalSetting = ref(
 	(includingTypes.value === null || includingTypes.value.length === 0) &&
 		props.showGlobalToggle,
@@ -89,7 +89,7 @@ function ok() {
 		});
 	}
 
-	dialog.value.close();
+	dialog.value!.close();
 }
 
 function disableAll() {
diff --git a/packages/client/src/components/MkNotificationToast.vue b/packages/client/src/components/MkNotificationToast.vue
index c5ca37d36a..a18a7e3d48 100644
--- a/packages/client/src/components/MkNotificationToast.vue
+++ b/packages/client/src/components/MkNotificationToast.vue
@@ -19,9 +19,10 @@ import { onMounted, ref } from "vue";
 import XNotification from "@/components/MkNotification.vue";
 import * as os from "@/os";
 import { defaultStore } from "@/store";
+import type { entities } from "firefish-js";
 
 defineProps<{
-	notification: any; // TODO
+	notification: entities.Notification;
 }>();
 
 const emit = defineEmits<{
diff --git a/packages/client/src/components/MkNotifications.vue b/packages/client/src/components/MkNotifications.vue
index 7b63b34197..43669c6607 100644
--- a/packages/client/src/components/MkNotifications.vue
+++ b/packages/client/src/components/MkNotifications.vue
@@ -44,7 +44,9 @@
 <script lang="ts" setup>
 import { computed, onMounted, onUnmounted, ref } from "vue";
 import type { StreamTypes, entities, notificationTypes } from "firefish-js";
-import MkPagination from "@/components/MkPagination.vue";
+import MkPagination, {
+	type MkPaginationType,
+} from "@/components/MkPagination.vue";
 import XNotification from "@/components/MkNotification.vue";
 import XList from "@/components/MkDateSeparatedList.vue";
 import XNote from "@/components/MkNote.vue";
@@ -59,7 +61,7 @@ const props = defineProps<{
 
 const stream = useStream();
 
-const pagingComponent = ref<InstanceType<typeof MkPagination>>();
+const pagingComponent = ref<MkPaginationType<"i/notifications"> | null>(null);
 
 const pagination = {
 	endpoint: "i/notifications" as const,
diff --git a/packages/client/src/components/MkPagination.vue b/packages/client/src/components/MkPagination.vue
index 143e3e1656..03a1f0e35f 100644
--- a/packages/client/src/components/MkPagination.vue
+++ b/packages/client/src/components/MkPagination.vue
@@ -67,7 +67,7 @@
 </template>
 
 <script lang="ts" setup generic="E extends PagingKey">
-import type { ComputedRef } from "vue";
+import type { ComponentPublicInstance, ComputedRef } from "vue";
 import { computed, isRef, onActivated, onDeactivated, ref, watch } from "vue";
 import type { Endpoints, TypeUtils } from "firefish-js";
 import * as os from "@/os";
@@ -81,8 +81,30 @@ import MkButton from "@/components/MkButton.vue";
 import { i18n } from "@/i18n";
 import { defaultStore } from "@/store";
 
+/**
+ * ref type of MkPagination<E>
+ * Due to Vue's incomplete type support for generic components,
+ * we have to manually maintain this type instead of
+ * using `InstanceType<typeof MkPagination>`
+ */
+export type MkPaginationType<
+	E extends PagingKey,
+	Item = Endpoints[E]["res"][number],
+> = ComponentPublicInstance & {
+	items: Item[];
+	queue: Item[];
+	backed: boolean;
+	reload: () => Promise<void>;
+	refresh: () => Promise<void>;
+	prepend: (item: Item) => Promise<void>;
+	append: (item: Item) => Promise<void>;
+	removeItem: (finder: (item: Item) => boolean) => boolean;
+	updateItem: (id: string, replacer: (old: Item) => Item) => boolean;
+};
+
+export type PagingKeyOf<T> = TypeUtils.EndpointsOf<T[]>;
 // biome-ignore lint/suspicious/noExplicitAny: Used Intentionally
-export type PagingKey = TypeUtils.EndpointsOf<any[]>;
+export type PagingKey = PagingKeyOf<any>;
 
 export interface Paging<E extends PagingKey = PagingKey> {
 	endpoint: E;
diff --git a/packages/client/src/components/MkPostFormDialog.vue b/packages/client/src/components/MkPostFormDialog.vue
index 380312ee13..cf6f45e5ed 100644
--- a/packages/client/src/components/MkPostFormDialog.vue
+++ b/packages/client/src/components/MkPostFormDialog.vue
@@ -2,7 +2,7 @@
 	<MkModal
 		ref="modal"
 		:prefer-type="'dialog'"
-		@click="modal.close()"
+		@click="modal!.close()"
 		@closed="onModalClosed()"
 	>
 		<MkPostForm
@@ -12,8 +12,8 @@
 			autofocus
 			freeze-after-posted
 			@posted="onPosted"
-			@cancel="modal.close()"
-			@esc="modal.close()"
+			@cancel="modal!.close()"
+			@esc="modal!.close()"
 		/>
 	</MkModal>
 </template>
diff --git a/packages/client/src/components/MkRenoteButton.vue b/packages/client/src/components/MkRenoteButton.vue
index 72a0559a0e..845298b89d 100644
--- a/packages/client/src/components/MkRenoteButton.vue
+++ b/packages/client/src/components/MkRenoteButton.vue
@@ -77,7 +77,7 @@ const hasRenotedBefore = ref(false);
 if (isSignedIn) {
 	os.api("notes/renotes", {
 		noteId: props.note.id,
-		userId: me.id,
+		userId: me!.id,
 		limit: 1,
 	}).then((res) => {
 		hasRenotedBefore.value = res.length > 0;
@@ -251,6 +251,10 @@ const renote = (viaKeyboard = false, ev?: MouseEvent) => {
 
 	os.popupMenu(buttonActions, buttonRef.value, { viaKeyboard });
 };
+
+defineExpose({
+	renote,
+});
 </script>
 
 <style lang="scss" scoped>
diff --git a/packages/client/src/components/MkSubNoteContent.vue b/packages/client/src/components/MkSubNoteContent.vue
index 3c1e2a418b..7d350085bb 100644
--- a/packages/client/src/components/MkSubNoteContent.vue
+++ b/packages/client/src/components/MkSubNoteContent.vue
@@ -31,7 +31,6 @@
 			:text="note.cw"
 			:author="note.user"
 			:lang="note.lang"
-			:i="me"
 			:custom-emojis="note.emojis"
 		/>
 	</p>
@@ -63,8 +62,8 @@
 			<div
 				class="body"
 				v-bind="{
-					'aria-hidden': note.cw && !showContent ? 'true' : null,
-					tabindex: !showContent ? '-1' : null,
+					'aria-hidden': note.cw && !showContent ? 'true' : undefined,
+					tabindex: !showContent ? '-1' : undefined,
 				}"
 			>
 				<span v-if="note.deletedAt" style="opacity: 0.5"
@@ -103,7 +102,6 @@
 					v-if="note.text"
 					:text="note.text"
 					:author="note.user"
-					:i="me"
 					:lang="note.lang"
 					:custom-emojis="note.emojis"
 				/>
@@ -256,7 +254,7 @@ async function toggleMfm() {
 }
 
 function focusFooter(ev) {
-	if (ev.key == "Tab" && !ev.getModifierState("Shift")) {
+	if (ev.key === "Tab" && !ev.getModifierState("Shift")) {
 		emit("focusfooter");
 	}
 }
diff --git a/packages/client/src/components/MkUserList.vue b/packages/client/src/components/MkUserList.vue
index 92eb5df953..5043b920ff 100644
--- a/packages/client/src/components/MkUserList.vue
+++ b/packages/client/src/components/MkUserList.vue
@@ -11,10 +11,10 @@
 			</div>
 		</template>
 
-		<template #default="{ items: users }">
+		<template #default="{ items }: { items: entities.UserDetailed[] }">
 			<div class="efvhhmdq">
 				<MkUserInfo
-					v-for="user in users"
+					v-for="user in items"
 					:key="user.id"
 					class="user"
 					:user="user"
@@ -27,16 +27,21 @@
 <script lang="ts" setup>
 import { ref } from "vue";
 import MkUserInfo from "@/components/MkUserInfo.vue";
-import type { Paging } from "@/components/MkPagination.vue";
+import type {
+	MkPaginationType,
+	PagingKeyOf,
+	PagingOf,
+} from "@/components/MkPagination.vue";
 import MkPagination from "@/components/MkPagination.vue";
 import { i18n } from "@/i18n";
+import type { entities } from "firefish-js";
 
 defineProps<{
-	pagination: Paging;
+	pagination: PagingOf<entities.UserDetailed>;
 	noGap?: boolean;
 }>();
 
-const pagingComponent = ref<InstanceType<typeof MkPagination>>();
+const pagingComponent = ref<MkPaginationType<PagingKeyOf<entities.User>>>();
 </script>
 
 <style lang="scss" scoped>
diff --git a/packages/client/src/components/MkUserSelectLocalDialog.vue b/packages/client/src/components/MkUserSelectLocalDialog.vue
index bd8494cb7a..1a8aff3c83 100644
--- a/packages/client/src/components/MkUserSelectLocalDialog.vue
+++ b/packages/client/src/components/MkUserSelectLocalDialog.vue
@@ -94,9 +94,9 @@ import { defaultStore } from "@/store";
 import { i18n } from "@/i18n";
 
 const emit = defineEmits<{
-	(ev: "ok", selected: entities.UserDetailed): void;
-	(ev: "cancel"): void;
-	(ev: "closed"): void;
+	ok: [selected: entities.UserDetailed];
+	cancel: [];
+	closed: [];
 }>();
 
 const username = ref("");
@@ -114,7 +114,7 @@ const search = () => {
 		query: username.value,
 		origin: "local",
 		limit: 10,
-		detail: false,
+		detail: true,
 	}).then((_users) => {
 		users.value = _users;
 	});
@@ -127,7 +127,7 @@ const ok = () => {
 
 	// 最近使ったユーザー更新
 	let recents = defaultStore.state.recentlyUsedUsers;
-	recents = recents.filter((x) => x !== selected.value.id);
+	recents = recents.filter((x) => x !== selected.value!.id);
 	recents.unshift(selected.value.id);
 	defaultStore.set("recentlyUsedUsers", recents.splice(0, 16));
 };
diff --git a/packages/client/src/components/global/MkMisskeyFlavoredMarkdown.vue b/packages/client/src/components/global/MkMisskeyFlavoredMarkdown.vue
index 3fe805b5b4..0dc8a40617 100644
--- a/packages/client/src/components/global/MkMisskeyFlavoredMarkdown.vue
+++ b/packages/client/src/components/global/MkMisskeyFlavoredMarkdown.vue
@@ -26,7 +26,7 @@ withDefaults(
 		text: string;
 		plain?: boolean;
 		nowrap?: boolean;
-		author?: entities.User;
+		author?: entities.User | null;
 		customEmojis?: entities.EmojiLite[];
 		isNote?: boolean;
 		advancedMfm?: boolean;
diff --git a/packages/client/src/components/mfm.ts b/packages/client/src/components/mfm.ts
index f2b100a207..0b31b29bfb 100644
--- a/packages/client/src/components/mfm.ts
+++ b/packages/client/src/components/mfm.ts
@@ -30,7 +30,7 @@ export default defineComponent({
 			default: false,
 		},
 		author: {
-			type: Object as PropType<entities.User>,
+			type: Object as PropType<entities.User | null>,
 			default: null,
 		},
 		// TODO: This variable is not used in the code and may be removed
diff --git a/packages/client/src/config.ts b/packages/client/src/config.ts
index e77ee87e85..1f9d6cdc7a 100644
--- a/packages/client/src/config.ts
+++ b/packages/client/src/config.ts
@@ -13,7 +13,7 @@ export const wsUrl = `${url
 export const lang = localStorage.getItem("lang");
 export const langs = _LANGS_;
 export const locale = JSON.parse(localStorage.getItem("locale") || "en-US");
-export const version = _VERSION_;
+export const version: string = _VERSION_;
 export const instanceName = siteName === "Firefish" ? host : siteName;
 export const ui = localStorage.getItem("ui");
 export const debug = localStorage.getItem("debug") === "true";
diff --git a/packages/client/src/os.ts b/packages/client/src/os.ts
index 49d03b76cc..04c658ec89 100644
--- a/packages/client/src/os.ts
+++ b/packages/client/src/os.ts
@@ -22,7 +22,7 @@ const apiClient = new firefishApi.APIClient({
 
 export const api = ((
 	endpoint: string,
-	data: Record<string, any> = {},
+	data: Record<string, unknown> = {},
 	token?: string | null | undefined,
 	useToken = true,
 ) => {
@@ -174,13 +174,14 @@ export function promiseDialog<T>(
 }
 
 let popupIdCount = 0;
-export const popups = ref<
-	{
-		id: number;
-		component: Component;
-		props: Record<string, unknown>;
-	}[]
->([]);
+
+type PopupType = {
+	id: number;
+	component: Component;
+	props: Record<string, unknown>;
+	events: Record<string, unknown>;
+};
+export const popups = ref<PopupType[]>([]);
 
 const zIndexes = {
 	low: 1000000,
@@ -922,18 +923,27 @@ export function contextMenu(
 	});
 }
 
-export function post(props: Record<string, any> = {}) {
-	return new Promise((resolve, reject) => {
+export function post(
+	props: InstanceType<typeof MkPostFormDialog>["$props"] = {},
+	onClosed?: () => void,
+) {
+	return new Promise<void>((resolve, reject) => {
 		// NOTE: MkPostFormDialogをdynamic importするとiOSでテキストエリアに自動フォーカスできない
 		// NOTE: ただ、dynamic importしない場合、MkPostFormDialogインスタンスが使いまわされ、
 		//       Vueが渡されたコンポーネントに内部的に__propsというプロパティを生やす影響で、
 		//       複数のpost formを開いたときに場合によってはエラーになる
 		//       もちろん複数のpost formを開けること自体Misskeyサイドのバグなのだが
-		let dispose;
+		// NOTE: Text area cannot be auto-focused on iOS when dynamically importing MkPostFormDialog
+		// NOTE: However, if you do not dynamically import, the MkPostFormDialog instance will be reused,
+		// Due to the effect that Vue internally creates a property called __props on the passed component,
+		// Sometimes an error occurs when opening multiple post forms
+		// Of course, opening multiple post forms is itself a bug on Misskey's side.
+		let dispose: () => void;
 		popup(MkPostFormDialog, props, {
 			closed: () => {
 				resolve();
 				dispose();
+				onClosed?.();
 			},
 		}).then((res) => {
 			dispose = res.dispose;
diff --git a/packages/client/src/pages/about.vue b/packages/client/src/pages/about.vue
index caf70856af..f757d3a1a5 100644
--- a/packages/client/src/pages/about.vue
+++ b/packages/client/src/pages/about.vue
@@ -176,6 +176,7 @@
 import { computed, onMounted, ref, watch } from "vue";
 import { Virtual } from "swiper/modules";
 import { Swiper, SwiperSlide } from "swiper/vue";
+import type { Swiper as SwiperType } from "swiper/types";
 import XEmojis from "./about.emojis.vue";
 import XFederation from "./about.federation.vue";
 import { host, version } from "@/config";
@@ -294,19 +295,19 @@ watch(iconSrc, (newValue, oldValue) => {
 	}
 });
 
-let swiperRef = null;
+let swiperRef: SwiperType | null = null;
 
-function setSwiperRef(swiper) {
+function setSwiperRef(swiper: SwiperType) {
 	swiperRef = swiper;
 	syncSlide(tabs.indexOf(tab.value));
 }
 
 function onSlideChange() {
-	tab.value = tabs[swiperRef.activeIndex];
+	tab.value = tabs[swiperRef!.activeIndex];
 }
 
-function syncSlide(index) {
-	swiperRef.slideTo(index);
+function syncSlide(index: number) {
+	swiperRef!.slideTo(index);
 }
 </script>
 
diff --git a/packages/client/src/pages/admin/abuses.vue b/packages/client/src/pages/admin/abuses.vue
index f94e24cf7c..72850ed478 100644
--- a/packages/client/src/pages/admin/abuses.vue
+++ b/packages/client/src/pages/admin/abuses.vue
@@ -94,14 +94,16 @@
 import { computed, ref } from "vue";
 
 import MkSelect from "@/components/form/select.vue";
-import MkPagination from "@/components/MkPagination.vue";
+import MkPagination, {
+	type MkPaginationType,
+} from "@/components/MkPagination.vue";
 import XAbuseReport from "@/components/MkAbuseReport.vue";
 import { i18n } from "@/i18n";
 import { definePageMetadata } from "@/scripts/page-metadata";
 import icon from "@/scripts/icon";
 import type { entities } from "firefish-js";
 
-const reports = ref<InstanceType<typeof MkPagination>>();
+const reports = ref<MkPaginationType<typeof pagination.endpoint> | null>(null);
 
 const state = ref("unresolved");
 const reporterOrigin = ref<entities.OriginType>("combined");
diff --git a/packages/client/src/pages/admin/emojis.vue b/packages/client/src/pages/admin/emojis.vue
index 7802e5aad6..869979063c 100644
--- a/packages/client/src/pages/admin/emojis.vue
+++ b/packages/client/src/pages/admin/emojis.vue
@@ -153,7 +153,9 @@
 import { computed, defineAsyncComponent, ref } from "vue";
 import MkButton from "@/components/MkButton.vue";
 import MkInput from "@/components/form/input.vue";
-import MkPagination from "@/components/MkPagination.vue";
+import MkPagination, {
+	type MkPaginationType,
+} from "@/components/MkPagination.vue";
 import MkSwitch from "@/components/form/switch.vue";
 import FormSplit from "@/components/form/split.vue";
 import { selectFile, selectFiles } from "@/scripts/select-file";
@@ -162,7 +164,8 @@ import { i18n } from "@/i18n";
 import { definePageMetadata } from "@/scripts/page-metadata";
 import icon from "@/scripts/icon";
 
-const emojisPaginationComponent = ref<InstanceType<typeof MkPagination>>();
+const emojisPaginationComponent =
+	ref<MkPaginationType<"admin/emoji/list"> | null>(null);
 
 const tab = ref("local");
 const query = ref(null);
diff --git a/packages/client/src/pages/admin/users.vue b/packages/client/src/pages/admin/users.vue
index d5028993e5..f72067b84e 100644
--- a/packages/client/src/pages/admin/users.vue
+++ b/packages/client/src/pages/admin/users.vue
@@ -126,7 +126,9 @@
 import { computed, ref } from "vue";
 import MkInput from "@/components/form/input.vue";
 import MkSelect from "@/components/form/select.vue";
-import MkPagination from "@/components/MkPagination.vue";
+import MkPagination, {
+	type MkPaginationType,
+} from "@/components/MkPagination.vue";
 import * as os from "@/os";
 import { lookupUser } from "@/scripts/lookup-user";
 import { i18n } from "@/i18n";
@@ -134,7 +136,9 @@ import { definePageMetadata } from "@/scripts/page-metadata";
 import MkUserCardMini from "@/components/MkUserCardMini.vue";
 import icon from "@/scripts/icon";
 
-const paginationComponent = ref<InstanceType<typeof MkPagination>>();
+const paginationComponent = ref<MkPaginationType<
+	typeof pagination.endpoint
+> | null>(null);
 
 const sort = ref("+createdAt");
 const state = ref("all");
diff --git a/packages/client/src/pages/announcements.vue b/packages/client/src/pages/announcements.vue
index df2a794ba4..59b703a35e 100644
--- a/packages/client/src/pages/announcements.vue
+++ b/packages/client/src/pages/announcements.vue
@@ -54,6 +54,7 @@
 <script lang="ts" setup>
 import { computed, ref } from "vue";
 import MkPagination from "@/components/MkPagination.vue";
+import type { MkPaginationType } from "@/components/MkPagination.vue";
 import MkButton from "@/components/MkButton.vue";
 import * as os from "@/os";
 import { i18n } from "@/i18n";
@@ -66,7 +67,7 @@ const pagination = {
 	limit: 10,
 };
 
-const paginationEl = ref<InstanceType<typeof MkPagination>>();
+const paginationEl = ref<MkPaginationType<"announcements"> | null>(null);
 function read(id: string) {
 	if (!paginationEl.value) return;
 	paginationEl.value.updateItem(id, (announcement) => {
diff --git a/packages/client/src/pages/favorites.vue b/packages/client/src/pages/favorites.vue
index f9fb36f296..09e857449b 100644
--- a/packages/client/src/pages/favorites.vue
+++ b/packages/client/src/pages/favorites.vue
@@ -37,6 +37,7 @@
 <script lang="ts" setup>
 import { ref } from "vue";
 import MkPagination from "@/components/MkPagination.vue";
+import type { MkPaginationType } from "@/components/MkPagination.vue";
 import XNote from "@/components/MkNote.vue";
 import XList from "@/components/MkDateSeparatedList.vue";
 import { i18n } from "@/i18n";
@@ -48,7 +49,9 @@ const pagination = {
 	limit: 10,
 };
 
-const pagingComponent = ref<InstanceType<typeof MkPagination>>();
+const pagingComponent = ref<MkPaginationType<
+	typeof pagination.endpoint
+> | null>(null);
 
 definePageMetadata({
 	title: i18n.ts.favorites,
diff --git a/packages/client/src/pages/follow-requests-sent.vue b/packages/client/src/pages/follow-requests-sent.vue
index 8ca769848b..b360c704f0 100644
--- a/packages/client/src/pages/follow-requests-sent.vue
+++ b/packages/client/src/pages/follow-requests-sent.vue
@@ -66,14 +66,17 @@
 import { computed, ref } from "vue";
 import { acct } from "firefish-js";
 import MkPagination from "@/components/MkPagination.vue";
+import type { MkPaginationType } from "@/components/MkPagination.vue";
 import { userPage } from "@/filters/user";
-import * as os from "@/os";
+// import * as os from "@/os";
 import { i18n } from "@/i18n";
 import { definePageMetadata } from "@/scripts/page-metadata";
 import { me } from "@/me";
 import icon from "@/scripts/icon";
 
-const paginationComponent = ref<InstanceType<typeof MkPagination>>();
+const paginationComponent = ref<MkPaginationType<
+	typeof pagination.endpoint
+> | null>(null);
 
 const pagination = {
 	endpoint: "following/requests/sent" as const,
diff --git a/packages/client/src/pages/follow-requests.vue b/packages/client/src/pages/follow-requests.vue
index d4999458c8..3abc414c0c 100644
--- a/packages/client/src/pages/follow-requests.vue
+++ b/packages/client/src/pages/follow-requests.vue
@@ -85,6 +85,7 @@
 import { computed, ref } from "vue";
 import { acct } from "firefish-js";
 import MkPagination from "@/components/MkPagination.vue";
+import type { MkPaginationType } from "@/components/MkPagination.vue";
 import { userPage } from "@/filters/user";
 import * as os from "@/os";
 import { i18n } from "@/i18n";
@@ -92,7 +93,9 @@ import { definePageMetadata } from "@/scripts/page-metadata";
 import { me } from "@/me";
 import icon from "@/scripts/icon";
 
-const paginationComponent = ref<InstanceType<typeof MkPagination>>();
+const paginationComponent = ref<MkPaginationType<
+	typeof pagination.endpoint
+> | null>(null);
 
 const pagination = {
 	endpoint: "following/requests/list" as const,
@@ -102,13 +105,13 @@ const pagination = {
 
 function accept(user) {
 	os.api("following/requests/accept", { userId: user.id }).then(() => {
-		paginationComponent.value.reload();
+		paginationComponent.value!.reload();
 	});
 }
 
 function reject(user) {
 	os.api("following/requests/reject", { userId: user.id }).then(() => {
-		paginationComponent.value.reload();
+		paginationComponent.value!.reload();
 	});
 }
 
diff --git a/packages/client/src/pages/messaging/messaging-room.vue b/packages/client/src/pages/messaging/messaging-room.vue
index fa61ec4bc3..bde0003f2f 100644
--- a/packages/client/src/pages/messaging/messaging-room.vue
+++ b/packages/client/src/pages/messaging/messaging-room.vue
@@ -110,7 +110,7 @@ import { acct } from "firefish-js";
 import XMessage from "./messaging-room.message.vue";
 import XForm from "./messaging-room.form.vue";
 import XList from "@/components/MkDateSeparatedList.vue";
-import type { Paging } from "@/components/MkPagination.vue";
+import type { MkPaginationType, Paging } from "@/components/MkPagination.vue";
 import MkPagination from "@/components/MkPagination.vue";
 import {
 	isBottomVisible,
@@ -136,7 +136,9 @@ const stream = useStream();
 
 const rootEl = ref<HTMLDivElement>();
 const formEl = ref<InstanceType<typeof XForm>>();
-const pagingComponent = ref<InstanceType<typeof MkPagination>>();
+const pagingComponent = ref<MkPaginationType<"messaging/messages"> | null>(
+	null,
+);
 
 const fetching = ref(true);
 const user = ref<entities.UserDetailed | null>(null);
diff --git a/packages/client/src/pages/my-antennas/index.vue b/packages/client/src/pages/my-antennas/index.vue
index a4787f7241..5b645479a9 100644
--- a/packages/client/src/pages/my-antennas/index.vue
+++ b/packages/client/src/pages/my-antennas/index.vue
@@ -54,7 +54,7 @@
 
 <script lang="ts" setup>
 import { computed, onActivated, onDeactivated, ref } from "vue";
-import MkPagination from "@/components/MkPagination.vue";
+import MkPagination, { MkPaginationType } from "@/components/MkPagination.vue";
 import MkButton from "@/components/MkButton.vue";
 import MkInfo from "@/components/MkInfo.vue";
 import { i18n } from "@/i18n";
@@ -70,7 +70,7 @@ const headerActions = computed(() => []);
 
 const headerTabs = computed(() => []);
 
-const list = ref<typeof MkPagination | null>(null);
+const list = ref<MkPaginationType<typeof pagination.endpoint> | null>(null);
 
 let isCached = false;
 let refreshTimer: number | null = null;
diff --git a/packages/client/src/pages/my-clips/index.vue b/packages/client/src/pages/my-clips/index.vue
index daaef6f3a0..d4eeb9e4a9 100644
--- a/packages/client/src/pages/my-clips/index.vue
+++ b/packages/client/src/pages/my-clips/index.vue
@@ -40,7 +40,9 @@
 <script lang="ts" setup>
 import { computed, ref } from "vue";
 
-import MkPagination from "@/components/MkPagination.vue";
+import MkPagination, {
+	type MkPaginationType,
+} from "@/components/MkPagination.vue";
 import MkInfo from "@/components/MkInfo.vue";
 import * as os from "@/os";
 import { i18n } from "@/i18n";
@@ -52,7 +54,9 @@ const pagination = {
 	limit: 10,
 };
 
-const pagingComponent = ref<InstanceType<typeof MkPagination>>();
+const pagingComponent = ref<MkPaginationType<
+	typeof pagination.endpoint
+> | null>(null);
 
 async function create() {
 	const { canceled, result } = await os.form(i18n.ts.createNewClip, {
diff --git a/packages/client/src/pages/my-lists/index.vue b/packages/client/src/pages/my-lists/index.vue
index 990ce45670..5c928ccc28 100644
--- a/packages/client/src/pages/my-lists/index.vue
+++ b/packages/client/src/pages/my-lists/index.vue
@@ -44,7 +44,9 @@
 <script lang="ts" setup>
 import { computed, ref } from "vue";
 
-import MkPagination from "@/components/MkPagination.vue";
+import MkPagination, {
+	type MkPaginationType,
+} from "@/components/MkPagination.vue";
 import MkButton from "@/components/MkButton.vue";
 import MkAvatars from "@/components/MkAvatars.vue";
 import MkInfo from "@/components/MkInfo.vue";
@@ -53,7 +55,9 @@ import { i18n } from "@/i18n";
 import { definePageMetadata } from "@/scripts/page-metadata";
 import icon from "@/scripts/icon";
 
-const pagingComponent = ref<InstanceType<typeof MkPagination>>();
+const pagingComponent = ref<MkPaginationType<
+	typeof pagination.endpoint
+> | null>(null);
 
 const pagination = {
 	endpoint: "users/lists/list" as const,
diff --git a/packages/client/src/pages/note-history.vue b/packages/client/src/pages/note-history.vue
index fdf8178fbf..e7a8b0d352 100644
--- a/packages/client/src/pages/note-history.vue
+++ b/packages/client/src/pages/note-history.vue
@@ -34,7 +34,9 @@
 
 <script lang="ts" setup>
 import { computed, onMounted, ref } from "vue";
-import MkPagination from "@/components/MkPagination.vue";
+import MkPagination, {
+	type MkPaginationType,
+} from "@/components/MkPagination.vue";
 import { api } from "@/os";
 import XList from "@/components/MkDateSeparatedList.vue";
 import XNote from "@/components/MkNote.vue";
@@ -43,7 +45,9 @@ import { definePageMetadata } from "@/scripts/page-metadata";
 import icon from "@/scripts/icon";
 import type { entities } from "firefish-js";
 
-const pagingComponent = ref<InstanceType<typeof MkPagination>>();
+const pagingComponent = ref<MkPaginationType<
+	typeof pagination.endpoint
+> | null>(null);
 
 const props = defineProps<{
 	noteId: string;
diff --git a/packages/client/src/pages/search.vue b/packages/client/src/pages/search.vue
index 972a2d46b2..c7fb067764 100644
--- a/packages/client/src/pages/search.vue
+++ b/packages/client/src/pages/search.vue
@@ -89,8 +89,9 @@ const usersPagination = {
 	endpoint: "users/search" as const,
 	limit: 10,
 	params: computed(() => ({
-		query: props.query,
-		origin: "combined",
+		// FIXME: query is necessary for user search
+		query: props.query!,
+		origin: "combined" as const,
 	})),
 };
 
diff --git a/packages/client/src/scripts/get-note-menu.ts b/packages/client/src/scripts/get-note-menu.ts
index 450a5885c4..873b32d7d7 100644
--- a/packages/client/src/scripts/get-note-menu.ts
+++ b/packages/client/src/scripts/get-note-menu.ts
@@ -13,16 +13,17 @@ import { getUserMenu } from "@/scripts/get-user-menu";
 import icon from "@/scripts/icon";
 import { useRouter } from "@/router";
 import { notePage } from "@/filters/note";
+import type { NoteTranslation } from "@/types/note";
 
 const router = useRouter();
 
 export function getNoteMenu(props: {
 	note: entities.Note;
 	menuButton: Ref<HTMLElement | undefined>;
-	translation: Ref<any>;
+	translation: Ref<NoteTranslation | null>;
 	translating: Ref<boolean>;
 	isDeleted: Ref<boolean>;
-	currentClipPage?: Ref<entities.Clip>;
+	currentClipPage?: Ref<entities.Clip> | null;
 }) {
 	const isRenote =
 		props.note.renote != null &&
diff --git a/packages/client/src/scripts/use-note-capture.ts b/packages/client/src/scripts/use-note-capture.ts
index def3baf8f8..1bc32d5246 100644
--- a/packages/client/src/scripts/use-note-capture.ts
+++ b/packages/client/src/scripts/use-note-capture.ts
@@ -6,7 +6,7 @@ import { isSignedIn, me } from "@/me";
 import * as os from "@/os";
 
 export function useNoteCapture(props: {
-	rootEl: Ref<HTMLElement>;
+	rootEl: Ref<HTMLElement | null>;
 	note: Ref<entities.Note>;
 	isDeletedRef: Ref<boolean>;
 }) {
diff --git a/packages/client/src/store.ts b/packages/client/src/store.ts
index deba55d285..85224f0d86 100644
--- a/packages/client/src/store.ts
+++ b/packages/client/src/store.ts
@@ -3,11 +3,24 @@ import { isSignedIn } from "./me";
 import { Storage } from "./pizzax";
 import type { NoteVisibility } from "@/types/note";
 
-export const postFormActions = [];
-export const userActions = [];
-export const noteActions = [];
-export const noteViewInterruptors = [];
-export const notePostInterruptors = [];
+export const postFormActions: {
+	title: string;
+	handler: (note: entities.Note) => void | Promise<void>;
+}[] = [];
+export const userActions: {
+	title: string;
+	handler: (note: entities.Note) => void | Promise<void>;
+}[] = [];
+export const noteActions: {
+	title: string;
+	handler: (note: entities.Note) => void | Promise<void>;
+}[] = [];
+export const noteViewInterruptors: {
+	handler: (note: entities.Note) => Promise<entities.Note>;
+}[] = [];
+export const notePostInterruptors: {
+	handler: (note: entities.Note) => Promise<entities.Note>;
+}[] = [];
 
 const menuOptions = [
 	"notifications",
@@ -453,6 +466,7 @@ import darkTheme from "@/themes/d-rosepine.json5";
  * Storage for configuration information that does not need to be constantly loaded into memory (non-reactive)
  */
 import lightTheme from "@/themes/l-rosepinedawn.json5";
+import { entities } from "firefish-js";
 
 export class ColdDeviceStorage {
 	public static default = {
diff --git a/packages/client/src/types/note.ts b/packages/client/src/types/note.ts
index 559ce0c793..7f4de74d77 100644
--- a/packages/client/src/types/note.ts
+++ b/packages/client/src/types/note.ts
@@ -1,3 +1,8 @@
 import type { noteVisibilities } from "firefish-js";
 
 export type NoteVisibility = (typeof noteVisibilities)[number] | "private";
+
+export type NoteTranslation = {
+	sourceLang: string;
+	text: string;
+};
diff --git a/packages/firefish-js/src/api.types.ts b/packages/firefish-js/src/api.types.ts
index 890e6bcd48..ffb22c191b 100644
--- a/packages/firefish-js/src/api.types.ts
+++ b/packages/firefish-js/src/api.types.ts
@@ -36,6 +36,7 @@ import type {
 	UserDetailed,
 	UserGroup,
 	UserList,
+	UserLite,
 	UserSorting,
 } from "./entities";
 
@@ -686,7 +687,14 @@ export type Endpoints = {
 		res: Note[];
 	};
 	"notes/clips": { req: TODO; res: TODO };
-	"notes/conversation": { req: TODO; res: TODO };
+	"notes/conversation": {
+		req: {
+			noteId: string;
+			limit?: number;
+			offset?: number;
+		};
+		res: Note[];
+	};
 	"notes/create": {
 		req: NoteSubmitReq;
 		res: { createdNote: Note };
@@ -789,7 +797,24 @@ export type Endpoints = {
 		res: Note[];
 	};
 	"notes/search-by-tag": { req: TODO; res: TODO };
-	"notes/search": { req: TODO; res: TODO };
+	"notes/search": {
+		req: {
+			query: string;
+			sinceId?: string;
+			untilId?: string;
+			sinceDate?: number;
+			untilDate?: number;
+			limit?: number;
+			offset?: number;
+			host?: string;
+			userId?: string;
+			withFiles?: boolean;
+			searchCwAndAlt?: boolean;
+			channelId?: string;
+			order?: "chronological" | "relevancy";
+		};
+		res: Note[];
+	};
 	"notes/show": { req: { noteId: Note["id"] }; res: Note };
 	"notes/state": { req: TODO; res: TODO };
 	"notes/timeline": {
@@ -802,6 +827,16 @@ export type Endpoints = {
 		};
 		res: Note[];
 	};
+	"notes/translate": {
+		req: {
+			noteId: string;
+			targetLang: string;
+		};
+		res: {
+			sourceLang: string;
+			text: string;
+		};
+	};
 	"notes/unrenote": { req: { noteId: Note["id"] }; res: null };
 	"notes/user-list-timeline": {
 		req: {
@@ -972,7 +1007,16 @@ export type Endpoints = {
 	"users/relation": { req: TODO; res: TODO };
 	"users/report-abuse": { req: TODO; res: TODO };
 	"users/search-by-username-and-host": { req: TODO; res: TODO };
-	"users/search": { req: TODO; res: TODO };
+	"users/search": {
+		req: {
+			query: string;
+			offset?: number;
+			limit?: number;
+			origin?: "local" | "remote" | "combined";
+			detail?: true; // FIXME: when false, returns UserLite
+		};
+		res: UserDetailed[];
+	};
 	"users/show": {
 		req: ShowUserReq | { userIds: User["id"][] };
 		res: {
diff --git a/packages/firefish-js/src/entities.ts b/packages/firefish-js/src/entities.ts
index 34c958ea5f..a8a15f2290 100644
--- a/packages/firefish-js/src/entities.ts
+++ b/packages/firefish-js/src/entities.ts
@@ -19,14 +19,7 @@ export type UserLite = {
 	alsoKnownAs: string[];
 	movedToUri: any;
 	emojis: EmojiLite[];
-	instance?: {
-		name: Instance["name"];
-		softwareName: Instance["softwareName"];
-		softwareVersion: Instance["softwareVersion"];
-		iconUrl: Instance["iconUrl"];
-		faviconUrl: Instance["faviconUrl"];
-		themeColor: Instance["themeColor"];
-	};
+	instance?: InstanceLite;
 };
 
 export type UserDetailed = UserLite & {
@@ -556,6 +549,15 @@ export type Blocking = {
 	blockee: UserDetailed;
 };
 
+export type InstanceLite = {
+	name: Instance["name"];
+	softwareName: Instance["softwareName"];
+	softwareVersion: Instance["softwareVersion"];
+	iconUrl: Instance["iconUrl"];
+	faviconUrl: Instance["faviconUrl"];
+	themeColor: Instance["themeColor"];
+};
+
 export type Instance = {
 	id: ID;
 	caughtAt: DateString;

From 695cb452bc7ddbc66fc8abaa304e54ea1b2f5c31 Mon Sep 17 00:00:00 2001
From: Lhcfl <Lhcfl@outlook.com>
Date: Fri, 12 Apr 2024 16:50:07 +0800
Subject: [PATCH 035/110] chore: Temporarily disable organizeImports

---
 biome.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/biome.json b/biome.json
index 80f0d63eb4..21b711f457 100644
--- a/biome.json
+++ b/biome.json
@@ -1,7 +1,7 @@
 {
 	"$schema": "https://biomejs.dev/schemas/1.6.4/schema.json",
 	"organizeImports": {
-		"enabled": true
+		"enabled": false
 	},
 	"linter": {
 		"enabled": true,

From f422842aefc234cf83dc264e3a8939ddad2e7e95 Mon Sep 17 00:00:00 2001
From: Lhcfl <Lhcfl@outlook.com>
Date: Fri, 12 Apr 2024 16:55:34 +0800
Subject: [PATCH 036/110] use Number.parseInt

---
 packages/client/src/components/MkChart.vue     |  6 +++---
 .../client/src/components/MkPollEditor.vue     |  2 +-
 packages/client/src/components/MkWindow.vue    | 18 +++++++++---------
 .../src/pages/admin/overview.queue-chart.vue   |  6 +++---
 .../src/pages/admin/queue.chart.chart.vue      |  6 +++---
 packages/client/src/scripts/2fa.ts             |  2 +-
 packages/client/src/scripts/color.ts           |  6 +++---
 packages/client/src/scripts/hpml/evaluator.ts  |  2 +-
 packages/client/src/scripts/hpml/lib.ts        |  4 ++--
 packages/client/src/scripts/physics.ts         |  6 +++---
 packages/client/src/scripts/popout.ts          |  4 ++--
 11 files changed, 31 insertions(+), 31 deletions(-)

diff --git a/packages/client/src/components/MkChart.vue b/packages/client/src/components/MkChart.vue
index d62f52d26f..abca01f199 100644
--- a/packages/client/src/components/MkChart.vue
+++ b/packages/client/src/components/MkChart.vue
@@ -100,9 +100,9 @@ const sum = (...arr) => arr.reduce((r, a) => r.map((b, i) => a[i] + b));
 const negate = (arr) => arr.map((x) => -x);
 const alpha = (hex, a) => {
 	const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)!;
-	const r = parseInt(result[1], 16);
-	const g = parseInt(result[2], 16);
-	const b = parseInt(result[3], 16);
+	const r = Number.parseInt(result[1], 16);
+	const g = Number.parseInt(result[2], 16);
+	const b = Number.parseInt(result[3], 16);
 	return `rgba(${r}, ${g}, ${b}, ${a})`;
 };
 
diff --git a/packages/client/src/components/MkPollEditor.vue b/packages/client/src/components/MkPollEditor.vue
index 51bc99ec77..982a432e26 100644
--- a/packages/client/src/components/MkPollEditor.vue
+++ b/packages/client/src/components/MkPollEditor.vue
@@ -147,7 +147,7 @@ function get() {
 	};
 
 	const calcAfter = () => {
-		let base = parseInt(after.value);
+		let base = Number.parseInt(after.value);
 		switch (unit.value) {
 			case "day":
 				base *= 24;
diff --git a/packages/client/src/components/MkWindow.vue b/packages/client/src/components/MkWindow.vue
index 147d0bec1f..8e0747abd5 100644
--- a/packages/client/src/components/MkWindow.vue
+++ b/packages/client/src/components/MkWindow.vue
@@ -271,7 +271,7 @@ function onHeaderMousedown(evt: MouseEvent) {
 			? evt.touches[0].clientY
 			: evt.clientY;
 	const moveBaseX = beforeMaximized
-		? parseInt(unMaximizedWidth, 10) / 2
+		? Number.parseInt(unMaximizedWidth, 10) / 2
 		: clickX - position.left; // TODO: parseIntやめる
 	const moveBaseY = beforeMaximized ? 20 : clickY - position.top;
 	const browserWidth = window.innerWidth;
@@ -321,8 +321,8 @@ function onTopHandleMousedown(evt) {
 	const main = rootEl.value;
 
 	const base = evt.clientY;
-	const height = parseInt(getComputedStyle(main, "").height, 10);
-	const top = parseInt(getComputedStyle(main, "").top, 10);
+	const height = Number.parseInt(getComputedStyle(main, "").height, 10);
+	const top = Number.parseInt(getComputedStyle(main, "").top, 10);
 
 	// 動かした時
 	dragListen((me) => {
@@ -349,8 +349,8 @@ function onRightHandleMousedown(evt) {
 	const main = rootEl.value;
 
 	const base = evt.clientX;
-	const width = parseInt(getComputedStyle(main, "").width, 10);
-	const left = parseInt(getComputedStyle(main, "").left, 10);
+	const width = Number.parseInt(getComputedStyle(main, "").width, 10);
+	const left = Number.parseInt(getComputedStyle(main, "").left, 10);
 	const browserWidth = window.innerWidth;
 
 	// 動かした時
@@ -375,8 +375,8 @@ function onBottomHandleMousedown(evt) {
 	const main = rootEl.value;
 
 	const base = evt.clientY;
-	const height = parseInt(getComputedStyle(main, "").height, 10);
-	const top = parseInt(getComputedStyle(main, "").top, 10);
+	const height = Number.parseInt(getComputedStyle(main, "").height, 10);
+	const top = Number.parseInt(getComputedStyle(main, "").top, 10);
 	const browserHeight = window.innerHeight;
 
 	// 動かした時
@@ -401,8 +401,8 @@ function onLeftHandleMousedown(evt) {
 	const main = rootEl.value;
 
 	const base = evt.clientX;
-	const width = parseInt(getComputedStyle(main, "").width, 10);
-	const left = parseInt(getComputedStyle(main, "").left, 10);
+	const width = Number.parseInt(getComputedStyle(main, "").width, 10);
+	const left = Number.parseInt(getComputedStyle(main, "").left, 10);
 
 	// 動かした時
 	dragListen((me) => {
diff --git a/packages/client/src/pages/admin/overview.queue-chart.vue b/packages/client/src/pages/admin/overview.queue-chart.vue
index cfbcbe6a11..f74cbb7e7e 100644
--- a/packages/client/src/pages/admin/overview.queue-chart.vue
+++ b/packages/client/src/pages/admin/overview.queue-chart.vue
@@ -47,9 +47,9 @@ const props = defineProps<{
 
 const alpha = (hex, a) => {
 	const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)!;
-	const r = parseInt(result[1], 16);
-	const g = parseInt(result[2], 16);
-	const b = parseInt(result[3], 16);
+	const r = Number.parseInt(result[1], 16);
+	const g = Number.parseInt(result[2], 16);
+	const b = Number.parseInt(result[3], 16);
 	return `rgba(${r}, ${g}, ${b}, ${a})`;
 };
 
diff --git a/packages/client/src/pages/admin/queue.chart.chart.vue b/packages/client/src/pages/admin/queue.chart.chart.vue
index b802457b4a..e8951709f2 100644
--- a/packages/client/src/pages/admin/queue.chart.chart.vue
+++ b/packages/client/src/pages/admin/queue.chart.chart.vue
@@ -47,9 +47,9 @@ const props = defineProps<{
 
 const alpha = (hex, a) => {
 	const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)!;
-	const r = parseInt(result[1], 16);
-	const g = parseInt(result[2], 16);
-	const b = parseInt(result[3], 16);
+	const r = Number.parseInt(result[1], 16);
+	const g = Number.parseInt(result[2], 16);
+	const b = Number.parseInt(result[3], 16);
 	return `rgba(${r}, ${g}, ${b}, ${a})`;
 };
 
diff --git a/packages/client/src/scripts/2fa.ts b/packages/client/src/scripts/2fa.ts
index 14d59bebec..9c34c8fb70 100644
--- a/packages/client/src/scripts/2fa.ts
+++ b/packages/client/src/scripts/2fa.ts
@@ -9,7 +9,7 @@ export function byteify(string: string, encoding: "ascii" | "base64" | "hex") {
 			);
 		case "hex":
 			return new Uint8Array(
-				string.match(/.{1,2}/g).map((byte) => parseInt(byte, 16)),
+				string.match(/.{1,2}/g).map((byte) => Number.parseInt(byte, 16)),
 			);
 	}
 }
diff --git a/packages/client/src/scripts/color.ts b/packages/client/src/scripts/color.ts
index 10a99a5a05..10b5ea0e54 100644
--- a/packages/client/src/scripts/color.ts
+++ b/packages/client/src/scripts/color.ts
@@ -1,7 +1,7 @@
 export const alpha = (hex: string, a: number): string => {
 	const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)!;
-	const r = parseInt(result[1], 16);
-	const g = parseInt(result[2], 16);
-	const b = parseInt(result[3], 16);
+	const r = Number.parseInt(result[1], 16);
+	const g = Number.parseInt(result[2], 16);
+	const b = Number.parseInt(result[3], 16);
 	return `rgba(${r}, ${g}, ${b}, ${a})`;
 };
diff --git a/packages/client/src/scripts/hpml/evaluator.ts b/packages/client/src/scripts/hpml/evaluator.ts
index ba06a87442..65f8f6a9f5 100644
--- a/packages/client/src/scripts/hpml/evaluator.ts
+++ b/packages/client/src/scripts/hpml/evaluator.ts
@@ -183,7 +183,7 @@ export class Hpml {
 			}
 
 			if (expr.type === "number") {
-				return parseInt(expr.value as any, 10);
+				return Number.parseInt(expr.value as any, 10);
 			}
 
 			if (expr.type === "text" || expr.type === "multiLineText") {
diff --git a/packages/client/src/scripts/hpml/lib.ts b/packages/client/src/scripts/hpml/lib.ts
index 0a2226be8a..06bda34655 100644
--- a/packages/client/src/scripts/hpml/lib.ts
+++ b/packages/client/src/scripts/hpml/lib.ts
@@ -505,7 +505,7 @@ export function initHpmlLib(
 		strReplace: (a: string, b: string, c: string) => a.split(b).join(c),
 		strReverse: (a: string) => a.split("").reverse().join(""),
 		join: (texts: string[], separator: string) => texts.join(separator || ""),
-		stringToNumber: (a: string) => parseInt(a),
+		stringToNumber: (a: string) => Number.parseInt(a),
 		numberToString: (a: number) => a.toString(),
 		splitStrByLine: (a: string) => a.split("\n"),
 		pick: (list: any[], i: number) => list[i - 1],
@@ -534,7 +534,7 @@ export function initHpmlLib(
 			let totalFactor = 0;
 			for (const x of list) {
 				const parts = x.split(" ");
-				const factor = parseInt(parts.pop()!, 10);
+				const factor = Number.parseInt(parts.pop()!, 10);
 				const text = parts.join(" ");
 				totalFactor += factor;
 				xs.push({ factor, text });
diff --git a/packages/client/src/scripts/physics.ts b/packages/client/src/scripts/physics.ts
index 5375c01d29..76dd117a79 100644
--- a/packages/client/src/scripts/physics.ts
+++ b/packages/client/src/scripts/physics.ts
@@ -65,10 +65,10 @@ export function physics(container: HTMLElement) {
 	const objs = [];
 	for (const objEl of objEls) {
 		const left = objEl.dataset.physicsX
-			? parseInt(objEl.dataset.physicsX)
+			? Number.parseInt(objEl.dataset.physicsX)
 			: objEl.offsetLeft;
 		const top = objEl.dataset.physicsY
-			? parseInt(objEl.dataset.physicsY)
+			? Number.parseInt(objEl.dataset.physicsY)
 			: objEl.offsetTop;
 
 		let obj;
@@ -90,7 +90,7 @@ export function physics(container: HTMLElement) {
 				objEl.offsetHeight,
 				{
 					chamfer: {
-						radius: parseInt(style.borderRadius || "0", 10),
+						radius: Number.parseInt(style.borderRadius || "0", 10),
 					},
 					restitution: 0.5,
 				},
diff --git a/packages/client/src/scripts/popout.ts b/packages/client/src/scripts/popout.ts
index 54aa422257..9113f4a9a4 100644
--- a/packages/client/src/scripts/popout.ts
+++ b/packages/client/src/scripts/popout.ts
@@ -9,8 +9,8 @@ export function popout(path: string, w?: HTMLElement) {
 	url = appendQuery(url, "zen");
 	if (w) {
 		const position = w.getBoundingClientRect();
-		const width = parseInt(getComputedStyle(w, "").width, 10);
-		const height = parseInt(getComputedStyle(w, "").height, 10);
+		const width = Number.parseInt(getComputedStyle(w, "").width, 10);
+		const height = Number.parseInt(getComputedStyle(w, "").height, 10);
 		const x = window.screenX + position.left;
 		const y = window.screenY + position.top;
 		window.open(

From 393ab2590d770eb590f5e4a8974e155cb9d09fa8 Mon Sep 17 00:00:00 2001
From: Lhcfl <Lhcfl@outlook.com>
Date: Fri, 12 Apr 2024 22:02:03 +0800
Subject: [PATCH 037/110] fix type errors of components

---
 packages/client/src/components/MkButton.vue   |  3 +-
 .../client/src/components/MkChartTooltip.vue  |  4 +-
 packages/client/src/components/MkDialog.vue   | 62 +++++++++++--------
 packages/client/src/components/MkDrive.vue    |  8 +--
 .../client/src/components/MkNotification.vue  | 54 +++++++++-------
 .../client/src/components/MkPagePreview.vue   |  5 +-
 .../client/src/components/MkPageWindow.vue    | 27 ++++----
 .../client/src/components/MkPollEditor.vue    | 19 +++---
 packages/client/src/components/MkPostForm.vue |  4 +-
 .../client/src/components/MkReactionIcon.vue  |  4 +-
 .../src/components/MkReactionTooltip.vue      |  6 +-
 .../components/MkReactionsViewer.details.vue  | 10 ++-
 .../components/MkReactionsViewer.reaction.vue |  2 +-
 .../client/src/components/MkRenoteButton.vue  |  2 +-
 packages/client/src/components/MkTooltip.vue  | 28 ++++++---
 .../client/src/components/MkUsersTooltip.vue  |  8 ++-
 packages/client/src/components/MkWidgets.vue  |  8 +--
 packages/client/src/components/global/MkA.vue |  2 +-
 packages/client/src/os.ts                     | 10 +--
 packages/firefish-js/src/entities.ts          | 16 ++++-
 20 files changed, 170 insertions(+), 112 deletions(-)

diff --git a/packages/client/src/components/MkButton.vue b/packages/client/src/components/MkButton.vue
index a0ff747afc..ab62fd166c 100644
--- a/packages/client/src/components/MkButton.vue
+++ b/packages/client/src/components/MkButton.vue
@@ -16,7 +16,7 @@
 		v-else
 		class="bghgjjyj _button"
 		:class="{ inline, primary, gradate, danger, rounded, full, mini }"
-		:to="to"
+		:to="to!"
 		@mousedown="onMousedown"
 	>
 		<div ref="ripples" class="ripples"></div>
@@ -36,6 +36,7 @@ const props = defineProps<{
 	gradate?: boolean;
 	rounded?: boolean;
 	inline?: boolean;
+	// FIXME: if `link`, `to` is necessary
 	link?: boolean;
 	to?: string;
 	autofocus?: boolean;
diff --git a/packages/client/src/components/MkChartTooltip.vue b/packages/client/src/components/MkChartTooltip.vue
index 659dc6d399..678a3ccdaa 100644
--- a/packages/client/src/components/MkChartTooltip.vue
+++ b/packages/client/src/components/MkChartTooltip.vue
@@ -28,11 +28,11 @@
 </template>
 
 <script lang="ts" setup>
-import {} from "vue";
+import type { Ref } from "vue";
 import MkTooltip from "./MkTooltip.vue";
 
 const props = defineProps<{
-	showing: boolean;
+	showing: Ref<boolean>;
 	x: number;
 	y: number;
 	title?: string;
diff --git a/packages/client/src/components/MkDialog.vue b/packages/client/src/components/MkDialog.vue
index 0b9d2cac36..c1f8f581a7 100644
--- a/packages/client/src/components/MkDialog.vue
+++ b/packages/client/src/components/MkDialog.vue
@@ -104,7 +104,7 @@
 			</MkInput>
 			<MkTextarea
 				v-if="input && input.type === 'paragraph'"
-				v-model="inputValue"
+				v-model="(inputValue as string)"
 				autofocus
 				type="paragraph"
 				:placeholder="input.placeholder || undefined"
@@ -204,7 +204,16 @@ import { i18n } from "@/i18n";
 import iconify from "@/scripts/icon";
 
 interface Input {
-	type: HTMLInputElement["type"];
+	type?:
+		| "text"
+		| "number"
+		| "password"
+		| "email"
+		| "url"
+		| "date"
+		| "time"
+		| "search"
+		| "paragraph";
 	placeholder?: string | null;
 	autocomplete?: string;
 	default: string | number | null;
@@ -237,8 +246,8 @@ const props = withDefaults(
 			| "question"
 			| "waiting"
 			| "search";
-		title: string;
-		text?: string;
+		title?: string | null;
+		text?: string | null;
 		isPlaintext?: boolean;
 		input?: Input;
 		select?: Select;
@@ -246,7 +255,7 @@ const props = withDefaults(
 		actions?: {
 			text: string;
 			primary?: boolean;
-			callback: (...args: any[]) => void;
+			callback: () => void;
 		}[];
 		showOkButton?: boolean;
 		showCancelButton?: boolean;
@@ -268,7 +277,10 @@ const props = withDefaults(
 );
 
 const emit = defineEmits<{
-	(ev: "done", v: { canceled: boolean; result: any }): void;
+	(
+		ev: "done",
+		v: { canceled: boolean; result?: string | number | boolean | null },
+	): void;
 	(ev: "closed"): void;
 }>();
 
@@ -306,7 +318,7 @@ const okButtonDisabled = computed<boolean>(() => {
 
 const inputEl = ref<typeof MkInput>();
 
-function done(canceled: boolean, result?) {
+function done(canceled: boolean, result?: string | number | boolean | null) {
 	emit("done", { canceled, result });
 	modal.value?.close(null);
 }
@@ -342,12 +354,12 @@ function onInputKeydown(evt: KeyboardEvent) {
 	}
 }
 
-function formatDateToYYYYMMDD(date) {
-	const year = date.getFullYear();
-	const month = ("0" + (date.getMonth() + 1)).slice(-2);
-	const day = ("0" + (date.getDate() + 1)).slice(-2);
-	return `${year}-${month}-${day}`;
-}
+// function formatDateToYYYYMMDD(date) {
+// 	const year = date.getFullYear();
+// 	const month = ("0" + (date.getMonth() + 1)).slice(-2);
+// 	const day = ("0" + (date.getDate() + 1)).slice(-2);
+// 	return `${year}-${month}-${day}`;
+// }
 
 /**
  * Appends a new search parameter to the value in the input field.
@@ -355,18 +367,18 @@ function formatDateToYYYYMMDD(date) {
  * begin typing a new criteria.
  * @param value The value to append.
  */
-function appendFilter(value: string) {
-	return (
-		[
-			typeof inputValue.value === "string"
-				? inputValue.value.trim()
-				: inputValue.value,
-			value,
-		]
-			.join(" ")
-			.trim() + " "
-	);
-}
+// function appendFilter(value: string) {
+// 	return (
+// 		[
+// 			typeof inputValue.value === "string"
+// 				? inputValue.value.trim()
+// 				: inputValue.value,
+// 			value,
+// 		]
+// 			.join(" ")
+// 			.trim() + " "
+// 	);
+// }
 
 onMounted(() => {
 	document.addEventListener("keydown", onKeydown);
diff --git a/packages/client/src/components/MkDrive.vue b/packages/client/src/components/MkDrive.vue
index 0273e0b40e..ad2f620f6c 100644
--- a/packages/client/src/components/MkDrive.vue
+++ b/packages/client/src/components/MkDrive.vue
@@ -253,7 +253,7 @@ function onStreamDriveFolderDeleted(folderId: string) {
 	removeFolder(folderId);
 }
 
-function onDragover(ev: DragEvent): any {
+function onDragover(ev: DragEvent) {
 	if (!ev.dataTransfer) return;
 
 	// ドラッグ元が自分自身の所有するアイテムだったら
@@ -285,7 +285,7 @@ function onDragleave() {
 	draghover.value = false;
 }
 
-function onDrop(ev: DragEvent): any {
+function onDrop(ev: DragEvent) {
 	draghover.value = false;
 
 	if (!ev.dataTransfer) return;
@@ -493,14 +493,12 @@ function move(target?: entities.DriveFolder) {
 	if (!target) {
 		goRoot();
 		return;
-	} else if (typeof target === "object") {
-		target = target.id;
 	}
 
 	fetching.value = true;
 
 	os.api("drive/folders/show", {
-		folderId: target,
+		folderId: target.id,
 	}).then((folderToMove) => {
 		folder.value = folderToMove;
 		hierarchyFolders.value = [];
diff --git a/packages/client/src/components/MkNotification.vue b/packages/client/src/components/MkNotification.vue
index b00646cdd6..6dd1a4b721 100644
--- a/packages/client/src/components/MkNotification.vue
+++ b/packages/client/src/components/MkNotification.vue
@@ -12,12 +12,12 @@
 				:user="notification.note.user"
 			/>
 			<MkAvatar
-				v-else-if="notification.user"
+				v-else-if="'user' in notification"
 				class="icon"
 				:user="notification.user"
 			/>
 			<img
-				v-else-if="notification.icon"
+				v-else-if="'icon' in notification && notification.icon"
 				class="icon"
 				:src="notification.icon"
 				alt=""
@@ -95,7 +95,7 @@
 					i18n.ts._notification.pollEnded
 				}}</span>
 				<MkA
-					v-else-if="notification.user"
+					v-else-if="'user' in notification"
 					v-user-preview="notification.user.id"
 					class="name"
 					:to="userPage(notification.user)"
@@ -133,7 +133,7 @@
 					:plain="true"
 					:nowrap="!full"
 					:lang="notification.note.lang"
-					:custom-emojis="notification.note.renote.emojis"
+					:custom-emojis="notification.note.renote!.emojis"
 				/>
 			</MkA>
 			<MkA
@@ -212,6 +212,7 @@
 				style="opacity: 0.7"
 				>{{ i18n.ts.youGotNewFollower }}
 				<div v-if="full && !hideFollowButton">
+					<!-- FIXME: Provide a UserDetailed here -->
 					<MkFollowButton
 						:user="notification.user"
 						:full="true"
@@ -269,7 +270,7 @@
 </template>
 
 <script lang="ts" setup>
-import { onMounted, onUnmounted, ref, watch } from "vue";
+import { onMounted, onUnmounted, ref, toRef, watch } from "vue";
 import type { entities } from "firefish-js";
 import XReactionIcon from "@/components/MkReactionIcon.vue";
 import MkFollowButton from "@/components/MkFollowButton.vue";
@@ -284,6 +285,8 @@ import { useTooltip } from "@/scripts/use-tooltip";
 import { defaultStore } from "@/store";
 import { instance } from "@/instance";
 import icon from "@/scripts/icon";
+import type { Connection } from "firefish-js/src/streaming";
+import type { Channels } from "firefish-js/src/streaming.types";
 
 const props = withDefaults(
 	defineProps<{
@@ -299,8 +302,8 @@ const props = withDefaults(
 
 const stream = useStream();
 
-const elRef = ref<HTMLElement>(null);
-const reactionRef = ref(null);
+const elRef = ref<HTMLElement | null>(null);
+const reactionRef = ref<InstanceType<typeof XReactionIcon> | null>(null);
 
 const hideFollowButton = defaultStore.state.hideFollowButtons;
 const showEmojiReactions =
@@ -311,7 +314,7 @@ const defaultReaction = ["⭐", "👍", "❤️"].includes(instance.defaultReact
 	: "⭐";
 
 let readObserver: IntersectionObserver | undefined;
-let connection;
+let connection: Connection<Channels["main"]> | null = null;
 
 onMounted(() => {
 	if (!props.notification.isRead) {
@@ -323,13 +326,13 @@ onMounted(() => {
 			observer.disconnect();
 		});
 
-		readObserver.observe(elRef.value);
+		readObserver.observe(elRef.value!);
 
 		connection = stream.useChannel("main");
-		connection.on("readAllNotifications", () => readObserver.disconnect());
+		connection.on("readAllNotifications", () => readObserver!.disconnect());
 
-		watch(props.notification.isRead, () => {
-			readObserver.disconnect();
+		watch(toRef(props.notification.isRead), () => {
+			readObserver!.disconnect();
 		});
 	}
 });
@@ -344,38 +347,47 @@ const groupInviteDone = ref(false);
 
 const acceptFollowRequest = () => {
 	followRequestDone.value = true;
-	os.api("following/requests/accept", { userId: props.notification.user.id });
+	os.api("following/requests/accept", {
+		userId: (props.notification as entities.ReceiveFollowRequestNotification)
+			.user.id,
+	});
 };
 
 const rejectFollowRequest = () => {
 	followRequestDone.value = true;
-	os.api("following/requests/reject", { userId: props.notification.user.id });
+	os.api("following/requests/reject", {
+		userId: (props.notification as entities.ReceiveFollowRequestNotification)
+			.user.id,
+	});
 };
 
 const acceptGroupInvitation = () => {
 	groupInviteDone.value = true;
 	os.apiWithDialog("users/groups/invitations/accept", {
-		invitationId: props.notification.invitation.id,
+		invitationId: (props.notification as entities.GroupInvitedNotification)
+			.invitation.id,
 	});
 };
 
 const rejectGroupInvitation = () => {
 	groupInviteDone.value = true;
 	os.api("users/groups/invitations/reject", {
-		invitationId: props.notification.invitation.id,
+		invitationId: (props.notification as entities.GroupInvitedNotification)
+			.invitation.id,
 	});
 };
 
 useTooltip(reactionRef, (showing) => {
+	const n = props.notification as entities.ReactionNotification;
 	os.popup(
 		XReactionTooltip,
 		{
 			showing,
-			reaction: props.notification.reaction
-				? props.notification.reaction.replace(/^:(\w+):$/, ":$1@.:")
-				: props.notification.reaction,
-			emojis: props.notification.note.emojis,
-			targetElement: reactionRef.value.$el,
+			reaction: n.reaction
+				? n.reaction.replace(/^:(\w+):$/, ":$1@.:")
+				: n.reaction,
+			emojis: n.note.emojis,
+			targetElement: reactionRef.value!.$el,
 		},
 		{},
 		"closed",
diff --git a/packages/client/src/components/MkPagePreview.vue b/packages/client/src/components/MkPagePreview.vue
index 034c6fed63..3a6a45745d 100644
--- a/packages/client/src/components/MkPagePreview.vue
+++ b/packages/client/src/components/MkPagePreview.vue
@@ -3,7 +3,7 @@
 		:to="`/@${page.user.username}/pages/${page.name}`"
 		class="vhpxefrj _block"
 		tabindex="-1"
-		:behavior="`${ui === 'deck' ? 'window' : null}`"
+		:behavior="ui === 'deck' ? 'window' : null"
 	>
 		<div
 			v-if="page.eyeCatchingImage"
@@ -36,9 +36,10 @@
 <script lang="ts" setup>
 import { userName } from "@/filters/user";
 import { ui } from "@/config";
+import type { entities } from "firefish-js";
 
 defineProps<{
-	page: any;
+	page: entities.Page;
 }>();
 </script>
 
diff --git a/packages/client/src/components/MkPageWindow.vue b/packages/client/src/components/MkPageWindow.vue
index d237f18091..082f9f0159 100644
--- a/packages/client/src/components/MkPageWindow.vue
+++ b/packages/client/src/components/MkPageWindow.vue
@@ -56,23 +56,22 @@ const router = new Router(routes, props.initialPath);
 
 const pageMetadata = ref<null | ComputedRef<PageMetadata>>();
 const windowEl = ref<InstanceType<typeof XWindow>>();
-const history = ref<{ path: string; key: any }[]>([
+const history = ref<{ path: string; key: string }[]>([
 	{
 		path: router.getCurrentPath(),
 		key: router.getCurrentKey(),
 	},
 ]);
 const buttonsLeft = computed(() => {
-	const buttons = [];
-
 	if (history.value.length > 1) {
-		buttons.push({
-			icon: `${icon("ph-caret-left")}`,
-			onClick: back,
-		});
+		return [
+			{
+				icon: `${icon("ph-caret-left")}`,
+				onClick: back,
+			},
+		];
 	}
-
-	return buttons;
+	return [];
 });
 const buttonsRight = computed(() => {
 	const buttons = [
@@ -114,7 +113,7 @@ const contextmenu = computed(() => [
 		text: i18n.ts.openInNewTab,
 		action: () => {
 			window.open(url + router.getCurrentPath(), "_blank");
-			windowEl.value.close();
+			windowEl.value!.close();
 		},
 	},
 	{
@@ -135,17 +134,17 @@ function back() {
 }
 
 function close() {
-	windowEl.value.close();
+	windowEl.value!.close();
 }
 
 function expand() {
 	mainRouter.push(router.getCurrentPath(), "forcePage");
-	windowEl.value.close();
+	windowEl.value!.close();
 }
 
 function popout() {
-	_popout(router.getCurrentPath(), windowEl.value.$el);
-	windowEl.value.close();
+	_popout(router.getCurrentPath(), windowEl.value!.$el);
+	windowEl.value!.close();
 }
 
 defineExpose({
diff --git a/packages/client/src/components/MkPollEditor.vue b/packages/client/src/components/MkPollEditor.vue
index 982a432e26..b05f80fafa 100644
--- a/packages/client/src/components/MkPollEditor.vue
+++ b/packages/client/src/components/MkPollEditor.vue
@@ -94,15 +94,14 @@ const props = defineProps<{
 	};
 }>();
 const emit = defineEmits<{
-	(
-		ev: "update:modelValue",
+	"update:modelValue": [
 		v: {
-			expiresAt: string;
-			expiredAfter: number;
+			expiresAt?: number;
+			expiredAfter?: number | null;
 			choices: string[];
 			multiple: boolean;
 		},
-	): void;
+	];
 }>();
 
 const choices = ref(props.modelValue.choices);
@@ -147,19 +146,19 @@ function get() {
 	};
 
 	const calcAfter = () => {
-		let base = Number.parseInt(after.value);
+		let base = Number.parseInt(after.value.toString());
 		switch (unit.value) {
+			// biome-ignore lint/suspicious/noFallthroughSwitchClause: Fallthrough intentially
 			case "day":
 				base *= 24;
-			// fallthrough
+			// biome-ignore lint/suspicious/noFallthroughSwitchClause: Fallthrough intentially
 			case "hour":
 				base *= 60;
-			// fallthrough
+			// biome-ignore lint/suspicious/noFallthroughSwitchClause: Fallthrough intentially
 			case "minute":
 				base *= 60;
-			// fallthrough
 			case "second":
-				return (base *= 1000);
+				return base * 1000;
 			default:
 				return null;
 		}
diff --git a/packages/client/src/components/MkPostForm.vue b/packages/client/src/components/MkPostForm.vue
index fb4a6dc740..2735eb55a3 100644
--- a/packages/client/src/components/MkPostForm.vue
+++ b/packages/client/src/components/MkPostForm.vue
@@ -1136,11 +1136,11 @@ async function post() {
 				nextTick(() => autosize.update(textareaEl.value));
 			});
 		})
-		.catch((err) => {
+		.catch((err: { message: string; id: string }) => {
 			posting.value = false;
 			os.alert({
 				type: "error",
-				text: err.message + "\n" + (err as any).id,
+				text: `${err.message}\n${err.id}`,
 			});
 		});
 	vibrate([10, 20, 10, 20, 10, 20, 60]);
diff --git a/packages/client/src/components/MkReactionIcon.vue b/packages/client/src/components/MkReactionIcon.vue
index e9d5a198cc..6608501478 100644
--- a/packages/client/src/components/MkReactionIcon.vue
+++ b/packages/client/src/components/MkReactionIcon.vue
@@ -9,9 +9,11 @@
 </template>
 
 <script lang="ts" setup>
+import type { entities } from "firefish-js";
+
 defineProps<{
 	reaction: string;
-	customEmojis?: any[]; // TODO
+	customEmojis?: entities.EmojiLite[];
 	noStyle?: boolean;
 }>();
 </script>
diff --git a/packages/client/src/components/MkReactionTooltip.vue b/packages/client/src/components/MkReactionTooltip.vue
index 0e83226c94..1286fc4c73 100644
--- a/packages/client/src/components/MkReactionTooltip.vue
+++ b/packages/client/src/components/MkReactionTooltip.vue
@@ -3,6 +3,7 @@
 		ref="tooltip"
 		:target-element="targetElement"
 		:max-width="340"
+		:showing="showing"
 		@closed="emit('closed')"
 	>
 		<div class="beeadbfb">
@@ -18,12 +19,15 @@
 </template>
 
 <script lang="ts" setup>
+import type { Ref } from "vue";
 import MkTooltip from "./MkTooltip.vue";
 import XReactionIcon from "@/components/MkReactionIcon.vue";
+import type { entities } from "firefish-js";
 
 defineProps<{
+	showing: Ref<boolean>;
 	reaction: string;
-	emojis: any[]; // TODO
+	emojis: entities.EmojiLite[];
 	targetElement: HTMLElement;
 }>();
 
diff --git a/packages/client/src/components/MkReactionsViewer.details.vue b/packages/client/src/components/MkReactionsViewer.details.vue
index 0d992ae431..14c2828d45 100644
--- a/packages/client/src/components/MkReactionsViewer.details.vue
+++ b/packages/client/src/components/MkReactionsViewer.details.vue
@@ -4,6 +4,7 @@
 		:target-element="targetElement"
 		:max-width="340"
 		@closed="emit('closed')"
+		:showing="showing"
 	>
 		<div class="bqxuuuey">
 			<div class="reaction">
@@ -29,15 +30,18 @@
 </template>
 
 <script lang="ts" setup>
+import type { Ref } from "vue";
 import MkTooltip from "./MkTooltip.vue";
 import XReactionIcon from "@/components/MkReactionIcon.vue";
+import type { entities } from "firefish-js";
 
 defineProps<{
+	showing: Ref<boolean>;
 	reaction: string;
-	users: any[]; // TODO
+	users: entities.User[]; // TODO
 	count: number;
-	emojis: any[]; // TODO
-	targetElement: HTMLElement;
+	emojis: entities.EmojiLite[]; // TODO
+	targetElement?: HTMLElement;
 }>();
 
 const emit = defineEmits<{
diff --git a/packages/client/src/components/MkReactionsViewer.reaction.vue b/packages/client/src/components/MkReactionsViewer.reaction.vue
index c403c7003c..89f51797ab 100644
--- a/packages/client/src/components/MkReactionsViewer.reaction.vue
+++ b/packages/client/src/components/MkReactionsViewer.reaction.vue
@@ -89,7 +89,7 @@ useTooltip(
 				emojis: props.note.emojis,
 				users,
 				count: props.count,
-				targetElement: buttonRef.value,
+				targetElement: buttonRef.value!,
 			},
 			{},
 			"closed",
diff --git a/packages/client/src/components/MkRenoteButton.vue b/packages/client/src/components/MkRenoteButton.vue
index 845298b89d..7250757da4 100644
--- a/packages/client/src/components/MkRenoteButton.vue
+++ b/packages/client/src/components/MkRenoteButton.vue
@@ -46,7 +46,7 @@ const buttonRef = ref<HTMLElement>();
 const canRenote = computed(
 	() =>
 		["public", "home"].includes(props.note.visibility) ||
-		props.note.userId === me.id,
+		props.note.userId === me?.id,
 );
 
 useTooltip(buttonRef, async (showing) => {
diff --git a/packages/client/src/components/MkTooltip.vue b/packages/client/src/components/MkTooltip.vue
index 883067c51f..2ed3c1974b 100644
--- a/packages/client/src/components/MkTooltip.vue
+++ b/packages/client/src/components/MkTooltip.vue
@@ -5,13 +5,13 @@
 		@after-leave="emit('closed')"
 	>
 		<div
-			v-show="showing"
+			v-show="unref(showing)"
 			ref="el"
 			class="buebdbiu _acrylic _shadow"
 			:style="{ zIndex, maxWidth: maxWidth + 'px' }"
 		>
 			<slot>
-				<Mfm v-if="asMfm" :text="text" />
+				<Mfm v-if="asMfm" :text="text!" />
 				<span v-else>{{ text }}</span>
 			</slot>
 		</div>
@@ -19,15 +19,22 @@
 </template>
 
 <script lang="ts" setup>
-import { nextTick, onMounted, onUnmounted, ref } from "vue";
+import {
+	type MaybeRef,
+	nextTick,
+	onMounted,
+	onUnmounted,
+	ref,
+	unref,
+} from "vue";
 import * as os from "@/os";
 import { calcPopupPosition } from "@/scripts/popup-position";
 import { defaultStore } from "@/store";
 
 const props = withDefaults(
 	defineProps<{
-		showing: boolean;
-		targetElement?: HTMLElement;
+		showing: MaybeRef<boolean>;
+		targetElement?: HTMLElement | null;
 		x?: number;
 		y?: number;
 		text?: string;
@@ -40,6 +47,7 @@ const props = withDefaults(
 		maxWidth: 250,
 		direction: "top",
 		innerMargin: 0,
+		targetElement: null,
 	},
 );
 
@@ -51,7 +59,7 @@ const el = ref<HTMLElement>();
 const zIndex = os.claimZIndex("high");
 
 function setPosition() {
-	const data = calcPopupPosition(el.value, {
+	const data = calcPopupPosition(el.value!, {
 		anchorElement: props.targetElement,
 		direction: props.direction,
 		align: "center",
@@ -60,12 +68,12 @@ function setPosition() {
 		y: props.y,
 	});
 
-	el.value.style.transformOrigin = data.transformOrigin;
-	el.value.style.left = data.left + "px";
-	el.value.style.top = data.top + "px";
+	el.value!.style.transformOrigin = data.transformOrigin;
+	el.value!.style.left = `${data.left}px`;
+	el.value!.style.top = `${data.top}px`;
 }
 
-let loopHandler;
+let loopHandler: number;
 
 onMounted(() => {
 	nextTick(() => {
diff --git a/packages/client/src/components/MkUsersTooltip.vue b/packages/client/src/components/MkUsersTooltip.vue
index 25af3ac121..741194221d 100644
--- a/packages/client/src/components/MkUsersTooltip.vue
+++ b/packages/client/src/components/MkUsersTooltip.vue
@@ -4,6 +4,7 @@
 		:target-element="targetElement"
 		:max-width="250"
 		@closed="emit('closed')"
+		:showing="showing"
 	>
 		<div class="beaffaef">
 			<div v-for="u in users" :key="u.id" class="user">
@@ -18,12 +19,15 @@
 </template>
 
 <script lang="ts" setup>
+import type { Ref } from "vue";
 import MkTooltip from "./MkTooltip.vue";
+import type { entities } from "firefish-js";
 
 defineProps<{
-	users: any[]; // TODO
+	showing: Ref<boolean>;
+	users: entities.User[];
 	count: number;
-	targetElement: HTMLElement;
+	targetElement?: HTMLElement;
 }>();
 
 const emit = defineEmits<{
diff --git a/packages/client/src/components/MkWidgets.vue b/packages/client/src/components/MkWidgets.vue
index 1b1dafa522..fb8a449590 100644
--- a/packages/client/src/components/MkWidgets.vue
+++ b/packages/client/src/components/MkWidgets.vue
@@ -85,7 +85,7 @@ import icon from "@/scripts/icon";
 interface Widget {
 	name: string;
 	id: string;
-	data: Record<string, any>;
+	data: Record<string, unknown>;
 }
 
 const props = defineProps<{
@@ -137,12 +137,12 @@ function onContextmenu(widget: Widget, ev: MouseEvent) {
 			return isLink(el.parentElement);
 		}
 	};
-	if (isLink(ev.target)) return;
+	if (isLink(ev.target as HTMLElement)) return;
 	if (
 		["INPUT", "TEXTAREA", "IMG", "VIDEO", "CANVAS"].includes(
-			ev.target.tagName,
+			(ev.target as HTMLElement).tagName,
 		) ||
-		ev.target.attributes.contenteditable
+		(ev.target as HTMLElement).getAttribute("contentEditable")
 	)
 		return;
 	if (window.getSelection()?.toString() !== "") return;
diff --git a/packages/client/src/components/global/MkA.vue b/packages/client/src/components/global/MkA.vue
index b85774dced..fbe5472a24 100644
--- a/packages/client/src/components/global/MkA.vue
+++ b/packages/client/src/components/global/MkA.vue
@@ -22,7 +22,7 @@ import icon from "@/scripts/icon";
 
 const props = withDefaults(
 	defineProps<{
-		to?: string;
+		to: string;
 		activeClass?: null | string;
 		behavior?: null | "window" | "browser" | "modalWindow";
 	}>(),
diff --git a/packages/client/src/os.ts b/packages/client/src/os.ts
index 04c658ec89..ac1227b827 100644
--- a/packages/client/src/os.ts
+++ b/packages/client/src/os.ts
@@ -210,11 +210,13 @@ interface VueComponentConstructor<P, E> {
 	emits?: E;
 }
 
+type NonArrayAble<A> = A extends Array<unknown> ? never : A;
+
 export async function popup<Props, Emits>(
 	component: VueComponentConstructor<Props, Emits>,
-	props: Props & Record<string, unknown>,
-	events: Partial<Emits> = {},
-	disposeEvent?: string,
+	props: Props,
+	events: Partial<NonArrayAble<NonNullable<Emits>>> = {},
+	disposeEvent?: keyof Partial<NonArrayAble<NonNullable<Emits>>>,
 ) {
 	markRaw(component);
 
@@ -227,7 +229,7 @@ export async function popup<Props, Emits>(
 	};
 	const state = {
 		component,
-		props,
+		props: props as Record<string, unknown>,
 		events: disposeEvent
 			? {
 					...events,
diff --git a/packages/firefish-js/src/entities.ts b/packages/firefish-js/src/entities.ts
index a8a15f2290..5858dacb43 100644
--- a/packages/firefish-js/src/entities.ts
+++ b/packages/firefish-js/src/entities.ts
@@ -20,6 +20,16 @@ export type UserLite = {
 	movedToUri: any;
 	emojis: EmojiLite[];
 	instance?: InstanceLite;
+	avatarColor: null;
+	emojiModPerm: "unauthorized" | "add" | "mod" | "full";
+	isAdmin?: boolean;
+	isModerator?: boolean;
+	isBot?: boolean;
+	isLocked: boolean;
+	isIndexable: boolean;
+	isCat?: boolean;
+	speakAsCat?: boolean;
+	driveCapacityOverrideMb: number | null,
 };
 
 export type UserDetailed = UserLite & {
@@ -46,7 +56,6 @@ export type UserDetailed = UserLite & {
 	isCat: boolean;
 	isFollowed: boolean;
 	isFollowing: boolean;
-	isLocked: boolean;
 	isModerator: boolean;
 	isMuted: boolean;
 	isRenoteMuted: boolean;
@@ -228,7 +237,10 @@ export interface RenoteNotification extends BaseNotification {
 	type: "renote";
 	user: User;
 	userId: User["id"];
-	note: Note;
+	note: Note & {
+		renote: Note,
+		renoteId: string,
+	};
 }
 export interface QuoteNotification extends BaseNotification {
 	type: "quote";

From 68be7d6be9f16329f2fe64a9ea4bbaf75c1f117f Mon Sep 17 00:00:00 2001
From: jolupa <jolupameister@gmail.com>
Date: Fri, 12 Apr 2024 06:38:32 +0000
Subject: [PATCH 038/110] locale: update translations (Catalan)

Currently translated at 100.0% (1920 of 1920 strings)

Translation: Firefish/locales
Translate-URL: https://hosted.weblate.org/projects/firefish/locales/ca/
---
 locales/ca-ES.yml | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/locales/ca-ES.yml b/locales/ca-ES.yml
index b8049c1b18..78ddcc2f55 100644
--- a/locales/ca-ES.yml
+++ b/locales/ca-ES.yml
@@ -2084,9 +2084,9 @@ _experiments:
   release: Publicà
   title: Experiments
   enablePostImports: Activar l'importació de publicacions
-  postImportsCaption: Permet els usuaris importar publicacions desde comptes a Firefish,
+  postImportsCaption: Permet als usuaris importar publicacions des de comptes de Firefish,
     Misskey, Mastodon, Akkoma i Pleroma. Pot fer que el servidor vagi més lent durant
-    la càrrega si tens un coll d'ampolla a la cua.
+    la importació si la teva cua de feina és saturada.
 noGraze: Si us plau, desactiva l'extensió del navegador "Graze for Mastodon", ja que
   interfereix amb Firefish.
 accessibility: Accessibilitat

From 2b88ef18a5e90c1a51e5de98b42759d96ece28d7 Mon Sep 17 00:00:00 2001
From: Lhcfl <Lhcfl@outlook.com>
Date: Sat, 13 Apr 2024 15:37:23 +0800
Subject: [PATCH 039/110] fix type errors of components

---
 packages/client/src/components/MkNoteSub.vue  |  2 +-
 packages/client/src/components/MkSignin.vue   | 10 +++----
 packages/client/src/components/MkSignup.vue   | 27 ++++++++++---------
 .../src/components/MkUserSelectDialog.vue     | 14 +++++-----
 .../src/components/MkVisibilityPicker.vue     | 10 +++----
 5 files changed, 32 insertions(+), 31 deletions(-)

diff --git a/packages/client/src/components/MkNoteSub.vue b/packages/client/src/components/MkNoteSub.vue
index ba91221b87..08c0bf6c38 100644
--- a/packages/client/src/components/MkNoteSub.vue
+++ b/packages/client/src/components/MkNoteSub.vue
@@ -414,7 +414,7 @@ function onContextmenu(ev: MouseEvent): void {
 						os.pageWindow(notePage(appearNote.value));
 					},
 				},
-				notePage(appearNote.value) != location.pathname
+				notePage(appearNote.value) !== location.pathname
 					? {
 							icon: `${icon("ph-arrows-out-simple")}`,
 							text: i18n.ts.showInPage,
diff --git a/packages/client/src/components/MkSignin.vue b/packages/client/src/components/MkSignin.vue
index f5c5f0ee4d..a49662d974 100644
--- a/packages/client/src/components/MkSignin.vue
+++ b/packages/client/src/components/MkSignin.vue
@@ -145,9 +145,10 @@ import * as os from "@/os";
 import { signIn } from "@/account";
 import { i18n } from "@/i18n";
 import icon from "@/scripts/icon";
+import type { entities } from "firefish-js";
 
 const signing = ref(false);
-const user = ref(null);
+const user = ref<entities.UserDetailed | null>(null);
 const username = ref("");
 const password = ref("");
 const token = ref("");
@@ -249,7 +250,7 @@ function queryKey() {
 function onSubmit() {
 	signing.value = true;
 	console.log("submit");
-	if (window.PublicKeyCredential && user.value.securityKeys) {
+	if (window.PublicKeyCredential && user.value?.securityKeys) {
 		os.api("signin", {
 			username: username.value,
 			password: password.value,
@@ -263,7 +264,7 @@ function onSubmit() {
 				return queryKey();
 			})
 			.catch(loginFailed);
-	} else if (!totpLogin.value && user.value && user.value.twoFactorEnabled) {
+	} else if (!totpLogin.value && user.value?.twoFactorEnabled) {
 		totpLogin.value = true;
 		signing.value = false;
 	} else {
@@ -272,8 +273,7 @@ function onSubmit() {
 			password: password.value,
 			"hcaptcha-response": hCaptchaResponse.value,
 			"g-recaptcha-response": reCaptchaResponse.value,
-			token:
-				user.value && user.value.twoFactorEnabled ? token.value : undefined,
+			token: user.value?.twoFactorEnabled ? token.value : undefined,
 		})
 			.then((res) => {
 				emit("login", res);
diff --git a/packages/client/src/components/MkSignup.vue b/packages/client/src/components/MkSignup.vue
index 6de17b2d48..9870263b7a 100644
--- a/packages/client/src/components/MkSignup.vue
+++ b/packages/client/src/components/MkSignup.vue
@@ -305,12 +305,12 @@ const host = toUnicode(config.host);
 const hcaptcha = ref();
 const recaptcha = ref();
 
-const username: string = ref("");
-const password: string = ref("");
-const retypedPassword: string = ref("");
-const invitationCode: string = ref("");
+const username = ref<string>("");
+const password = ref<string>("");
+const retypedPassword = ref<string>("");
+const invitationCode = ref<string>("");
 const email = ref("");
-const usernameState:
+const usernameState = ref<
 	| null
 	| "wait"
 	| "ok"
@@ -318,9 +318,10 @@ const usernameState:
 	| "error"
 	| "invalid-format"
 	| "min-range"
-	| "max-range" = ref(null);
-const invitationState: null | "entered" = ref(null);
-const emailState:
+	| "max-range"
+	>(null);
+const invitationState = ref<null | "entered">(null);
+const emailState = ref<
 	| null
 	| "wait"
 	| "ok"
@@ -330,11 +331,11 @@ const emailState:
 	| "unavailable:mx"
 	| "unavailable:smtp"
 	| "unavailable"
-	| "error" = ref(null);
-const passwordStrength: "" | "low" | "medium" | "high" = ref("");
-const passwordRetypeState: null | "match" | "not-match" = ref(null);
-const submitting: boolean = ref(false);
-const ToSAgreement: boolean = ref(false);
+	| "error">(null);
+const passwordStrength = ref<"" | "low" | "medium" | "high">("");
+const passwordRetypeState = ref<null | "match" | "not-match" >(null);
+const submitting = ref(false);
+const ToSAgreement = ref(false);
 const hCaptchaResponse = ref(null);
 const reCaptchaResponse = ref(null);
 
diff --git a/packages/client/src/components/MkUserSelectDialog.vue b/packages/client/src/components/MkUserSelectDialog.vue
index 3017e9bd6c..aa3a4c63c1 100644
--- a/packages/client/src/components/MkUserSelectDialog.vue
+++ b/packages/client/src/components/MkUserSelectDialog.vue
@@ -98,16 +98,16 @@ import { defaultStore } from "@/store";
 import { i18n } from "@/i18n";
 
 const emit = defineEmits<{
-	(ev: "ok", selected: entities.UserDetailed): void;
-	(ev: "cancel"): void;
-	(ev: "closed"): void;
+	ok: [selected: entities.UserDetailed];
+	cancel: [];
+	closed: [];
 }>();
 
 const username = ref("");
 const host = ref("");
-const users: entities.UserDetailed[] = ref([]);
-const recentUsers: entities.UserDetailed[] = ref([]);
-const selected: entities.UserDetailed | null = ref(null);
+const users = ref<entities.UserDetailed[]>([]);
+const recentUsers = ref<entities.UserDetailed[]>([]);
+const selected = ref<entities.UserDetailed | null>(null);
 const dialogEl = ref();
 
 const search = () => {
@@ -132,7 +132,7 @@ const ok = () => {
 
 	// 最近使ったユーザー更新
 	let recents = defaultStore.state.recentlyUsedUsers;
-	recents = recents.filter((x) => x !== selected.value.id);
+	recents = recents.filter((x) => x !== selected.value!.id);
 	recents.unshift(selected.value.id);
 	defaultStore.set("recentlyUsedUsers", recents.splice(0, 16));
 };
diff --git a/packages/client/src/components/MkVisibilityPicker.vue b/packages/client/src/components/MkVisibilityPicker.vue
index 0e6f8cb153..088da07eb0 100644
--- a/packages/client/src/components/MkVisibilityPicker.vue
+++ b/packages/client/src/components/MkVisibilityPicker.vue
@@ -3,7 +3,7 @@
 		ref="modal"
 		:z-priority="'high'"
 		:src="src"
-		@click="modal.close()"
+		@click="modal!.close()"
 		@closed="emit('closed')"
 	>
 		<div class="_popup" :class="$style.root">
@@ -159,9 +159,9 @@ const props = withDefaults(
 );
 
 const emit = defineEmits<{
-	(ev: "changeVisibility", v: NoteVisibility): void;
-	(ev: "changeLocalOnly", v: boolean): void;
-	(ev: "closed"): void;
+	changeVisibility: [v: NoteVisibility];
+	changeLocalOnly: [v: boolean];
+	closed: [];
 }>();
 
 const v = ref(props.currentVisibility);
@@ -175,7 +175,7 @@ function choose(visibility: NoteVisibility): void {
 	v.value = visibility;
 	emit("changeVisibility", visibility);
 	nextTick(() => {
-		modal.value.close();
+		modal.value!.close();
 	});
 }
 </script>

From baba86a2030d306fb382d355f17d8b410c62afd5 Mon Sep 17 00:00:00 2001
From: Lhcfl <Lhcfl@outlook.com>
Date: Sat, 13 Apr 2024 17:55:40 +0800
Subject: [PATCH 040/110] Downgrade vue-tsc

new versions of vue-tsc have perfomance issues:
see https://github.com/vuejs/language-tools/issues/4223
---
 packages/client/package.json |  2 +-
 pnpm-lock.yaml               | 34 +++++++++++++++++-----------------
 2 files changed, 18 insertions(+), 18 deletions(-)

diff --git a/packages/client/package.json b/packages/client/package.json
index 594f07c607..154d8f33cc 100644
--- a/packages/client/package.json
+++ b/packages/client/package.json
@@ -88,6 +88,6 @@
 		"vue-draggable-plus": "^0.4.0",
 		"vue-plyr": "^7.0.0",
 		"vue-prism-editor": "2.0.0-alpha.2",
-		"vue-tsc": "2.0.12"
+		"vue-tsc": "2.0.7"
 	}
 }
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 192b5f95e5..d39e984e81 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -783,8 +783,8 @@ importers:
         specifier: 2.0.0-alpha.2
         version: 2.0.0-alpha.2(vue@3.4.21)
       vue-tsc:
-        specifier: 2.0.12
-        version: 2.0.12(typescript@5.4.5)
+        specifier: 2.0.7
+        version: 2.0.7(typescript@5.4.5)
 
   packages/firefish-js:
     dependencies:
@@ -4833,22 +4833,22 @@ packages:
       vue: 3.4.21(typescript@5.4.5)
     dev: true
 
-  /@volar/language-core@2.2.0-alpha.7:
-    resolution: {integrity: sha512-igpp+nTkyl8faVzRJMpSCeA4XlBJ5UVSyc/WGyksmUmP10YbfufbcQCFlxEXv2uMBV+a3L4JVCj+Vju+08FOSA==}
+  /@volar/language-core@2.1.6:
+    resolution: {integrity: sha512-pAlMCGX/HatBSiDFMdMyqUshkbwWbLxpN/RL7HCQDOo2gYBE+uS+nanosLc1qR6pTQ/U8q00xt8bdrrAFPSC0A==}
     dependencies:
-      '@volar/source-map': 2.2.0-alpha.7
+      '@volar/source-map': 2.1.6
     dev: true
 
-  /@volar/source-map@2.2.0-alpha.7:
-    resolution: {integrity: sha512-iIZM2EovdEnr6mMwlsnt4ciix4xz7HSGHyUSviRaY5cii5PMXGHeUU9UDeb+xzLCx8kdk3L5J4z+ts50AhkYcg==}
+  /@volar/source-map@2.1.6:
+    resolution: {integrity: sha512-TeyH8pHHonRCHYI91J7fWUoxi0zWV8whZTVRlsWHSYfjm58Blalkf9LrZ+pj6OiverPTmrHRkBsG17ScQyWECw==}
     dependencies:
       muggle-string: 0.4.1
     dev: true
 
-  /@volar/typescript@2.2.0-alpha.7:
-    resolution: {integrity: sha512-qy04/hx4UbW1BdPlzaxlH60D4plubcyqdbYM6Y5vZiascZxFowtd6vE39Td9FYzDxwcKgzb/Crvf/ABhdHnuBA==}
+  /@volar/typescript@2.1.6:
+    resolution: {integrity: sha512-JgPGhORHqXuyC3r6skPmPHIZj4LoMmGlYErFTuPNBq9Nhc9VTv7ctHY7A3jMN3ngKEfRrfnUcwXHztvdSQqNfw==}
     dependencies:
-      '@volar/language-core': 2.2.0-alpha.7
+      '@volar/language-core': 2.1.6
       path-browserify: 1.0.1
     dev: true
 
@@ -4898,15 +4898,15 @@ packages:
       '@vue/shared': 3.4.21
     dev: true
 
-  /@vue/language-core@2.0.12(typescript@5.4.5):
-    resolution: {integrity: sha512-aIStDPt69SHOpiIckGTIIjEz/sXc6ZfCMS5uWYL1AcbcRMhzFCLZscGAVte1+ad+RRFepSpKBjGttyPcgKJ7ww==}
+  /@vue/language-core@2.0.7(typescript@5.4.5):
+    resolution: {integrity: sha512-Vh1yZX3XmYjn9yYLkjU8DN6L0ceBtEcapqiyclHne8guG84IaTzqtvizZB1Yfxm3h6m7EIvjerLO5fvOZO6IIQ==}
     peerDependencies:
       typescript: '*'
     peerDependenciesMeta:
       typescript:
         optional: true
     dependencies:
-      '@volar/language-core': 2.2.0-alpha.7
+      '@volar/language-core': 2.1.6
       '@vue/compiler-dom': 3.4.21
       '@vue/shared': 3.4.21
       computeds: 0.0.1
@@ -17161,14 +17161,14 @@ packages:
       he: 1.2.0
     dev: true
 
-  /vue-tsc@2.0.12(typescript@5.4.5):
-    resolution: {integrity: sha512-thlBBWlPYrNdba535oDdxz7PRUufZgRZRVP5Aql5wBVpGSWSeqou4EzFXeKVoZr59lp9hJROubDVzlhACmcEhg==}
+  /vue-tsc@2.0.7(typescript@5.4.5):
+    resolution: {integrity: sha512-LYa0nInkfcDBB7y8jQ9FQ4riJTRNTdh98zK/hzt4gEpBZQmf30dPhP+odzCa+cedGz6B/guvJEd0BavZaRptjg==}
     hasBin: true
     peerDependencies:
       typescript: '*'
     dependencies:
-      '@volar/typescript': 2.2.0-alpha.7
-      '@vue/language-core': 2.0.12(typescript@5.4.5)
+      '@volar/typescript': 2.1.6
+      '@vue/language-core': 2.0.7(typescript@5.4.5)
       semver: 7.6.0
       typescript: 5.4.5
     dev: true

From d0de0d14b29f8dc96090520fe448c50d62d2e301 Mon Sep 17 00:00:00 2001
From: naskya <m@naskya.net>
Date: Sat, 13 Apr 2024 19:41:30 +0900
Subject: [PATCH 041/110] docs: fix indent

---
 docs/notice-for-admins.md | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/docs/notice-for-admins.md b/docs/notice-for-admins.md
index abfaaea2fc..c22b1da0aa 100644
--- a/docs/notice-for-admins.md
+++ b/docs/notice-for-admins.md
@@ -21,8 +21,8 @@ The number of posts stored on your database can be found at `https://yourserver.
 
 - Please remove `packages/backend-rs/target` before building Firefish.
     ```sh
-		rm --recursive --force packages/backend-rs/target
-		```
+    rm --recursive --force packages/backend-rs/target
+    ```
 - Please do not terminate `pnpm run migrate` even if it appears to be frozen.
 
 ### For Docker/Podman users

From d57c9dc2890d3e3c3e30e7292d3f486cb2631639 Mon Sep 17 00:00:00 2001
From: naskya <m@naskya.net>
Date: Sat, 13 Apr 2024 19:46:31 +0900
Subject: [PATCH 042/110] fix (client): set displayMode to true for block math
 expressions

---
 packages/client/src/components/MkFormulaCore.vue | 6 ++----
 1 file changed, 2 insertions(+), 4 deletions(-)

diff --git a/packages/client/src/components/MkFormulaCore.vue b/packages/client/src/components/MkFormulaCore.vue
index 2db4c7d00d..507740d106 100644
--- a/packages/client/src/components/MkFormulaCore.vue
+++ b/packages/client/src/components/MkFormulaCore.vue
@@ -20,12 +20,10 @@ export default defineComponent({
 	},
 	computed: {
 		compiledFormula(): any {
-			const katexString = katex.renderToString(this.formula, {
+			return katex.renderToString(this.formula, {
 				throwOnError: false,
+				displayMode: this.block,
 			} as any);
-			return this.block
-				? `<div style="text-align:center">${katexString}</div>`
-				: katexString;
 		},
 	},
 });

From 799ad1f3f8fe9a7bda99144415fdade3c7d934be Mon Sep 17 00:00:00 2001
From: naskya <m@naskya.net>
Date: Sat, 13 Apr 2024 19:49:31 +0900
Subject: [PATCH 043/110] dev (minor, backend-rs): format Makefile

---
 packages/backend-rs/Makefile | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/packages/backend-rs/Makefile b/packages/backend-rs/Makefile
index f91a56aae7..eb4b30c6df 100644
--- a/packages/backend-rs/Makefile
+++ b/packages/backend-rs/Makefile
@@ -7,9 +7,9 @@ SRC += $(call recursive_wildcard, src, *)
 .PHONY: regenerate-entities
 regenerate-entities:
 	sea-orm-cli generate entity \
-		--output-dir='src/model/entity' \
-		--database-url='postgres://$(POSTGRES_USER):$(POSTGRES_PASSWORD)@localhost:25432/$(POSTGRES_DB)' \
-		--date-time-crate='chrono' \
+	  --output-dir='src/model/entity' \
+	  --database-url='postgres://$(POSTGRES_USER):$(POSTGRES_PASSWORD)@localhost:25432/$(POSTGRES_DB)' \
+	  --date-time-crate='chrono' \
 	  --model-extra-attributes='NAPI_EXTRA_ATTR_PLACEHOLDER' && \
 	for file in src/model/entity/*; do \
 	  base=$$(basename -- "$${file}"); \

From ad58ae8f3077136cbead48fdbd9024d4d7ab7cbc Mon Sep 17 00:00:00 2001
From: naskya <m@naskya.net>
Date: Sat, 13 Apr 2024 20:03:02 +0900
Subject: [PATCH 044/110] refactor: remove /api/patrons endpoint

---
 docs/api-change.md                            |   4 +
 locales/ar-SA.yml                             |   3 -
 locales/bn-BD.yml                             |   2 -
 locales/ca-ES.yml                             |   6 -
 locales/de-DE.yml                             |   6 -
 locales/en-US.yml                             |   6 -
 locales/es-ES.yml                             |   6 -
 locales/fr-FR.yml                             |   6 -
 locales/id-ID.yml                             |   6 -
 locales/it-IT.yml                             |   6 -
 locales/ja-JP.yml                             |   4 -
 locales/ja-KS.yml                             |   2 -
 locales/ko-KR.yml                             |   4 -
 locales/no-NO.yml                             |   6 -
 locales/pl-PL.yml                             |   3 -
 locales/ru-RU.yml                             |   6 -
 locales/sk-SK.yml                             |   3 -
 locales/th-TH.yml                             |   3 -
 locales/tr-TR.yml                             |   6 -
 locales/uk-UA.yml                             |   6 -
 locales/vi-VN.yml                             |   5 -
 locales/zh-CN.yml                             |   4 -
 locales/zh-TW.yml                             |   4 -
 packages/backend/src/server/api/endpoints.ts  |   2 -
 .../src/server/api/endpoints/patrons.ts       |  33 -----
 packages/client/src/pages/user/home.vue       |  28 -----
 patrons.json                                  | 118 ------------------
 27 files changed, 4 insertions(+), 284 deletions(-)
 delete mode 100644 packages/backend/src/server/api/endpoints/patrons.ts
 delete mode 100644 patrons.json

diff --git a/docs/api-change.md b/docs/api-change.md
index fc82e12806..e788b3d637 100644
--- a/docs/api-change.md
+++ b/docs/api-change.md
@@ -2,6 +2,10 @@
 
 Breaking changes are indicated by the :warning: icon.
 
+## Unreleased
+
+- :warning: Removed `patrons` endpoint.
+
 ## v20240405
 
 - Added `notes/history` endpoint.
diff --git a/locales/ar-SA.yml b/locales/ar-SA.yml
index 35fe237190..622074056c 100644
--- a/locales/ar-SA.yml
+++ b/locales/ar-SA.yml
@@ -893,9 +893,6 @@ _aboutFirefish:
   source: "الشفرة المصدرية"
   translation: "ترجم ميسكي"
   donate: "تبرع لميسكي"
-  morePatrons: "نحن نقدر الدعم الذي قدمه العديد من الأشخاص الذين لم نذكرهم. شكرًا
-    لكم 🥰"
-  patrons: "الداعمون"
 _nsfw:
   respect: "اخف الوسائط ذات المحتوى الحساس"
   ignore: "اعرض الوسائط ذات المحتوى الحساس"
diff --git a/locales/bn-BD.yml b/locales/bn-BD.yml
index d0869736ef..d17766b499 100644
--- a/locales/bn-BD.yml
+++ b/locales/bn-BD.yml
@@ -975,8 +975,6 @@ _aboutFirefish:
   source: "সোর্স কোড"
   translation: "Firefish অনুবাদ করুন"
   donate: "Firefish তে দান করুন"
-  morePatrons: "আরও অনেকে আমাদের সাহায্য করছেন। তাদের সবাইকে ধন্যবাদ 🥰"
-  patrons: "সমর্থনকারী"
 _nsfw:
   respect: "স্পর্শকাতর মিডিয়া লুকান"
   ignore: "স্পর্শকাতর মিডিয়া লুকাবেন না"
diff --git a/locales/ca-ES.yml b/locales/ca-ES.yml
index 78ddcc2f55..47328cb554 100644
--- a/locales/ca-ES.yml
+++ b/locales/ca-ES.yml
@@ -1586,18 +1586,12 @@ _aboutFirefish:
   translation: Tradueix Firefish
   about: Firefish és una bifurcació de Misskey feta per ThatOneCalculator, que està
     en desenvolupament des del 2022.
-  morePatrons: També agraïm el suport de molts altres ajudants que no figuren aquí.
-    Gràcies! 🥰
-  patrons: Mecenes de Firefish
-  patronsList: Llistats cronològicament, no per la quantitat donada. Fes una donació
-    amb l'enllaç de dalt per veure el teu nom aquí!
   donateTitle: T'agrada Firefish?
   pleaseDonateToFirefish: Penseu en fer una donació a Firefish per donar suport al
     seu desenvolupament.
   pleaseDonateToHost: Penseu també en fer una donació a la vostre instància, {host},
     per ajudar-lo a suportar els costos de funcionament.
   donateHost: Fes una donació a {host}
-  sponsors: Patrocinadors de Calckey
   misskeyContributors: Col·laboradors de Misskey
 unknown: Desconegut
 pageLikesCount: Nombre de pàgines amb M'agrada
diff --git a/locales/de-DE.yml b/locales/de-DE.yml
index 10901246e1..012f1ef3ea 100644
--- a/locales/de-DE.yml
+++ b/locales/de-DE.yml
@@ -1092,17 +1092,11 @@ _aboutFirefish:
   source: "Quellcode"
   translation: "Firefish übersetzen"
   donate: "An Firefish spenden"
-  morePatrons: "Wir schätzen ebenso die Unterstützung vieler anderer hier nicht gelisteter
-    Personen sehr. Danke! 🥰"
-  patrons: "UnterstützerInnen"
-  patronsList: Auflistung chonologisch, nicht nach Spenden-Größe. Spende über den
-    Link oben, um hier aufgeführt zu werden!
   donateTitle: Gefällt dir Firefish?
   pleaseDonateToFirefish: Bitte erwäge eine Spende an Firefish, um dessen Entwicklung
     zu unterstützen.
   pleaseDonateToHost: Bitte erwäge auch, an deinen Heimatserver {host} zu spenden,
     um bei der Deckung der Betriebskosten zu helfen.
-  sponsors: Firefish-Sponsoren
   donateHost: Spende an {host}
   misskeyContributors: Misskey-Mitwirkende
 _nsfw:
diff --git a/locales/en-US.yml b/locales/en-US.yml
index f8da18450c..d552e1e3a2 100644
--- a/locales/en-US.yml
+++ b/locales/en-US.yml
@@ -1336,12 +1336,6 @@ _aboutFirefish:
   pleaseDonateToHost: "Please also consider donating to your home server, {host},
     to help support its operation costs."
   donateHost: "Donate to {host}"
-  morePatrons: "We also appreciate the support of many other helpers not listed here.
-    Thank you! 🥰"
-  sponsors: "Firefish sponsors"
-  patrons: "Firefish patrons"
-  patronsList: "Listed chronologically, not by donation size. Donate with the link
-    above to get your name on here!"
 _nsfw:
   respect: "Hide NSFW media"
   ignore: "Don't hide NSFW media"
diff --git a/locales/es-ES.yml b/locales/es-ES.yml
index bfc2268a27..9980607521 100644
--- a/locales/es-ES.yml
+++ b/locales/es-ES.yml
@@ -1073,17 +1073,11 @@ _aboutFirefish:
   source: "Código fuente"
   translation: "Traducir Firefish"
   donate: "Donar a Firefish"
-  morePatrons: "También apreciamos el apoyo de muchos más que no están enlistados
-    aquí. ¡Gracias! 🥰"
-  patrons: "Mecenas de Firefish"
   pleaseDonateToFirefish: Por favor considera donar a Firefish para apollar su desarrollo.
   donateHost: Dona a {host}
-  patronsList: Listados cronológicamente no por monto de la donación. ¡Dona con el
-    vínculo de arriba para que tu nombre aparezca aquí!
   donateTitle: ¿Te gusta Firefish?
   pleaseDonateToHost: También considera donar a tu propio servidor , {host}, para
     ayudar con los costos de operación.
-  sponsors: Patrocinadores de Firefish
   misskeyContributors: Contribuidores de Misskey
 _nsfw:
   respect: "Ocultar medios NSFW"
diff --git a/locales/fr-FR.yml b/locales/fr-FR.yml
index da56856077..9dff23f110 100644
--- a/locales/fr-FR.yml
+++ b/locales/fr-FR.yml
@@ -996,18 +996,12 @@ _aboutFirefish:
   source: "Code source"
   translation: "Traduire Firefish"
   donate: "Soutenir Firefish"
-  morePatrons: "Nous apprécions vraiment le soutien de nombreuses autres personnes
-    non mentionnées ici. Merci à toutes et à tous ! 🥰"
-  patrons: "Contributeurs"
   pleaseDonateToFirefish: Merci de considérer de faire un don pour soutenir le développement
     de Firefish.
-  sponsors: Sponsors Firefish
   donateTitle: Firefish vous plaît ?
   pleaseDonateToHost: Également, veuillez envisager de faire un don à votre serveur
     d'accueil, {host}, pour contribuer à couvrir ses frais de fonctionnement.
   donateHost: Faire un don à {host}
-  patronsList: Listé chronologiquement, pas par taille de donation. Faite un don avec
-    le lien ci-dessus pour avoir votre nom affiché ici !
   misskeyContributors: Contributeurs Misskey
 _nsfw:
   respect: "Cacher les médias marqués comme contenu sensible (NSFW)"
diff --git a/locales/id-ID.yml b/locales/id-ID.yml
index 20efbdc458..4535492e7d 100644
--- a/locales/id-ID.yml
+++ b/locales/id-ID.yml
@@ -985,12 +985,6 @@ _aboutFirefish:
   source: "Sumber kode"
   translation: "Terjemahkan Firefish"
   donate: "Donasi ke Firefish"
-  morePatrons: "Kami sangat mengapresiasi dukungan dari banyak penolong lain yang
-    tidak tercantum disini. Terima kasih! 🥰"
-  patrons: "Pendukung"
-  patronsList: Diurutkan secara kronologis, bukan berdasarkan jumlah donasi. Berdonasilah
-    dengan tautan di atas supaya nama kamu ada di sini!
-  sponsors: Sponsor Firefish
   donateTitle: Suka Firefish?
   pleaseDonateToFirefish: Silakan pertimbangkan berdonasi ke Firefish untuk mendukung
     pengembangannya.
diff --git a/locales/it-IT.yml b/locales/it-IT.yml
index 2bd5d1e1a2..e686df5890 100644
--- a/locales/it-IT.yml
+++ b/locales/it-IT.yml
@@ -934,18 +934,12 @@ _aboutFirefish:
   source: "Codice sorgente"
   translation: "Traduzione di Firefish"
   donate: "Sostieni Firefish"
-  morePatrons: "Apprezziamo sinceramente l'aiuto di tante altre persone non elencate
-    qui. Grazie mille! 🥰"
-  patrons: "Sostenitori"
-  sponsors: Gli sponsor di Firefish
   misskeyContributors: Contributori di Misskey
   donateTitle: Ti piace Firefish?
   pleaseDonateToFirefish: Con una donazione puoi supportare lo sviluppo di Firefish.
   pleaseDonateToHost: Considera anche una donazione al server che ti ospita, {host},
     per contribuire ai costi che sostiene.
   donateHost: Dona a {host}
-  patronsList: Elencati in ordine cronologico, non per importo. Dona con il link sopra
-    per apparire in questa lista!
 _nsfw:
   respect: "Nascondi i media sensibli (NSFW)"
   ignore: "Mostra i media sensibili (NSFW)"
diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml
index 372dddf579..3f900d1a5e 100644
--- a/locales/ja-JP.yml
+++ b/locales/ja-JP.yml
@@ -1109,14 +1109,10 @@ _aboutFirefish:
   source: "ソースコード"
   translation: "Firefishを翻訳"
   donate: "Firefishに寄付"
-  morePatrons: "他にも多くの方が支援してくれています。ありがとうございます! 🥰"
-  patrons: "支援者"
-  patronsList: 寄付額ではなく時系列順に並んでいます。上記のリンクから寄付を行ってここにあなたのIDを載せましょう!
   pleaseDonateToFirefish: Firefish開発への寄付をご検討ください。
   pleaseDonateToHost: また、このサーバー {host} の運営者への寄付もご検討ください。
   donateHost: '{host} に寄付する'
   donateTitle: Firefishを気に入りましたか?
-  sponsors: Firefish の支援者
 _nsfw:
   respect: "閲覧注意のメディアは隠す"
   ignore: "閲覧注意のメディアを隠さない"
diff --git a/locales/ja-KS.yml b/locales/ja-KS.yml
index 8a39260db1..970a27d0ed 100644
--- a/locales/ja-KS.yml
+++ b/locales/ja-KS.yml
@@ -874,8 +874,6 @@ _aboutFirefish:
   source: "ソースコード"
   translation: "Firefishを翻訳"
   donate: "Firefishに寄付"
-  morePatrons: "他にもぎょうさんの人からサポートしてもろてんねん。ほんまおおきに🥰"
-  patrons: "支援者"
   misskeyContributors: フォーク元のMisskeyを作らはった人ら
 _mfm:
   cheatSheet: "MFMチートシート"
diff --git a/locales/ko-KR.yml b/locales/ko-KR.yml
index d9750a53df..d67e37d4cf 100644
--- a/locales/ko-KR.yml
+++ b/locales/ko-KR.yml
@@ -992,10 +992,6 @@ _aboutFirefish:
   source: "소스 코드"
   translation: "Firefish를 번역하기"
   donate: "Firefish에 기부하기"
-  morePatrons: "이 외에도 다른 많은 분들이 도움을 주시고 계십니다. 감사합니다🥰"
-  patrons: "후원자"
-  patronsList: 기부 금액이 아닌 시간 순서로 정렬합니다. 위 링크를 통해 후원하여 당신의 이름을 새겨 보세요!
-  sponsors: Firefish 스폰서
   pleaseDonateToHost: 또한, 이 서버 {host} 의 운영자에게 기부하는 것도 검토하여 주십시오.
   pleaseDonateToFirefish: Firefish의 개발에 후원하는 것을 검토하여 주십시오.
   donateHost: '{host} 에게 기부하기'
diff --git a/locales/no-NO.yml b/locales/no-NO.yml
index 1321d46a3f..b446ff4359 100644
--- a/locales/no-NO.yml
+++ b/locales/no-NO.yml
@@ -987,8 +987,6 @@ _aboutFirefish:
   pleaseDonateToFirefish: Du kan vurdere å donere en slant til Firefish for å støtte
     videre utvikling og feilretting.
   donateHost: Donér til {host}
-  morePatrons: Vi er også takknemlige for bidragene fra mange andre som ikke er listet
-    her. Takk til dere alle! 🥰
   contributors: Hovedutviklere
   source: Kildekode
   allContributors: Alle bidragsytere
@@ -996,10 +994,6 @@ _aboutFirefish:
   pleaseDonateToHost: Du kan også vurdere å donere til hjemme-tjeneren din, {host},
     for å hjelpe dem med driftskostnadene for tjenesten.
   about: Firefish ble opprettet av ThatOneCalculator i 2022, basert på Misskey.
-  sponsors: Firefishs sponsorer
-  patrons: Firefishs patroner
-  patronsList: Listen er kronologisk, ikke etter donert beløp. Doner med lenken over
-    for å få navnet ditt her!
 isBot: Denne kontoen er en bot
 _nsfw:
   respect: Skjul NSFW-merket media
diff --git a/locales/pl-PL.yml b/locales/pl-PL.yml
index fb57556f06..43eeb3c8f0 100644
--- a/locales/pl-PL.yml
+++ b/locales/pl-PL.yml
@@ -990,9 +990,6 @@ _aboutFirefish:
   source: "Kod źródłowy"
   translation: "Tłumacz Firefish"
   donate: "Przekaż darowiznę na Firefish"
-  morePatrons: "Naprawdę doceniam wsparcie ze strony wielu niewymienionych tu osób.
-    Dziękuję! 🥰"
-  patrons: "Wspierający"
 _nsfw:
   respect: "Ukrywaj media NSFW"
   ignore: "Nie ukrywaj mediów NSFW"
diff --git a/locales/ru-RU.yml b/locales/ru-RU.yml
index 2650aa714a..5c81e19a04 100644
--- a/locales/ru-RU.yml
+++ b/locales/ru-RU.yml
@@ -986,12 +986,6 @@ _aboutFirefish:
   source: "Исходный код"
   translation: "Перевод Firefish"
   donate: "Пожертвование на Firefish"
-  morePatrons: "Большое спасибо и многим другим, кто принял участие в этом проекте!
-    🥰"
-  patrons: "Материальная поддержка"
-  patronsList: Перечислены в хронологическом порядке, а не по размеру пожертвования.
-    Сделайте взнос по ссылке выше, чтобы ваше имя было здесь!
-  sponsors: Спонсоры Firefish
   donateTitle: Понравился Firefish?
   pleaseDonateToFirefish: Пожалуйста, поддержите разработку Firefish.
   pleaseDonateToHost: Также не забудьте поддержать ваш домашний сервер {host}, чтобы
diff --git a/locales/sk-SK.yml b/locales/sk-SK.yml
index 71513af387..e1ed198e4e 100644
--- a/locales/sk-SK.yml
+++ b/locales/sk-SK.yml
@@ -1036,9 +1036,6 @@ _aboutFirefish:
   source: "Zdrojový kód"
   translation: "Preložiť Firefish"
   donate: "Podporiť Firefish"
-  morePatrons: "Takisto oceňujeme podporu mnoých ďalších, ktorí tu nie sú uvedení.
-    Ďakujeme! 🥰"
-  patrons: "Prispievatelia"
 _nsfw:
   respect: "Skryť NSFW médiá"
   ignore: "Neskrývať NSFW médiá"
diff --git a/locales/th-TH.yml b/locales/th-TH.yml
index fa096a2c4d..4a668f910a 100644
--- a/locales/th-TH.yml
+++ b/locales/th-TH.yml
@@ -1022,9 +1022,6 @@ _aboutFirefish:
   source: "ซอร์สโค้ด"
   translation: "รับแปลภาษา Firefish"
   donate: "บริจาคให้กับ Firefish"
-  morePatrons: "เราขอขอบคุณสำหรับความช่วยเหลือจากผู้ช่วยอื่นๆ ที่ไม่ได้ระบุไว้ที่นี่นะ
-    ขอขอบคุณ! 🥰"
-  patrons: "สมาชิกพันธมิตร"
 _nsfw:
   respect: "ซ่อนสื่อ NSFW"
   ignore: "อย่าซ่อนสื่อ NSFW"
diff --git a/locales/tr-TR.yml b/locales/tr-TR.yml
index 148dbb4757..cb36a6a07c 100644
--- a/locales/tr-TR.yml
+++ b/locales/tr-TR.yml
@@ -1910,14 +1910,9 @@ _preferencesBackups:
   updatedAt: 'Güncelleme tarihi: {date} {time}'
   cannotLoad: Yüklenemedi
 _aboutFirefish:
-  patronsList: Bağış büyüklüğüne göre değil, kronolojik olarak listelenmiştir. Adınızı
-    buraya almak için yukarıdaki bağlantıyla bağış yapın!
   about: Firefish, 2022'den beri geliştirilmekte olan ThatOneCalculator tarafından
     yapılan bir Misskey çatalıdır.
   allContributors: Tüm katkıda bulunanlar
-  patrons: Firefish patronları
-  morePatrons: Burada listelenmeyen diğer birçok yardımcının desteğini de takdir ediyoruz.
-    Teşekkür ederim! 🥰
   donate: Firefish'e bağışta bulunun
   contributors: Ana katkıda bulunanlar
   source: Kaynak Kodu
@@ -1928,7 +1923,6 @@ _aboutFirefish:
   pleaseDonateToHost: İşletme maliyetlerini desteklemek için lütfen ev sunucunuz {host}'a
     bağış yapmayı da düşünün.
   donateHost: '{ev sahibi} için bağış yapın'
-  sponsors: Firefish sponsorları
   misskeyContributors: Misskey'e katkıda bulunanlar
 _weekday:
   saturday: Cumartesi
diff --git a/locales/uk-UA.yml b/locales/uk-UA.yml
index 64db5e0022..aa669f596f 100644
--- a/locales/uk-UA.yml
+++ b/locales/uk-UA.yml
@@ -825,17 +825,11 @@ _aboutFirefish:
   source: "Вихідний код"
   translation: "Перекладати Firefish"
   donate: "Пожертвувати Firefish"
-  morePatrons: "Ми дуже цінуємо підтримку багатьох інших помічників, не перелічених
-    тут. Дякуємо! 🥰"
-  patrons: "Підтримали"
-  patronsList: Перераховані в хронологічному порядку, а не за розміром пожертви. Зробіть
-    внесок за посиланням вище, щоб ваше ім'я було тут!
   donateTitle: Сподобався Firefish?
   pleaseDonateToFirefish: Будь ласка, підтримайте розробку Firefish.
   pleaseDonateToHost: Також не забудьте підтримати ваш домашній сервер {host}, щоб
     допомогти з його операційними витратами.
   donateHost: Зробити внесок на рахунок {host}
-  sponsors: Спонсори Firefish
   misskeyContributors: Контрибутори Misskey
 _nsfw:
   respect: "Приховувати NSFW медіа"
diff --git a/locales/vi-VN.yml b/locales/vi-VN.yml
index c6899c5b16..4c6a01f1ae 100644
--- a/locales/vi-VN.yml
+++ b/locales/vi-VN.yml
@@ -1050,15 +1050,10 @@ _aboutFirefish:
   source: "Mã nguồn"
   translation: "Dịch Firefish"
   donate: "Ủng hộ Firefish"
-  morePatrons: "Chúng tôi cũng trân trọng sự hỗ trợ của nhiều người đóng góp khác
-    không được liệt kê ở đây. Cảm ơn! 🥰"
-  patrons: "Người ủng hộ"
-  patronsList: Liệt kê theo thứ tự, không theo số tiền ủng hộ. Hãy để tên bạn ở đây!
   donateTitle: Thích Firefish?
   pleaseDonateToFirefish: Hãy cân nhắc ủng hộ Firefish phát triển.
   donateHost: Ủng hộ {host}
   pleaseDonateToHost: Cũng như ủng hộ chi phí vận hành máy chủ {host} của bạn.
-  sponsors: Nhà tài trợ Firefish
   misskeyContributors: Người đóng góp Misskey
 _nsfw:
   respect: "Ẩn nội dung NSFW"
diff --git a/locales/zh-CN.yml b/locales/zh-CN.yml
index a8510612dc..c496d05381 100644
--- a/locales/zh-CN.yml
+++ b/locales/zh-CN.yml
@@ -995,10 +995,6 @@ _aboutFirefish:
   source: "源代码"
   translation: "翻译 Firefish"
   donate: "赞助 Firefish"
-  morePatrons: "还有很多其它的人也在支持我们,非常感谢🥰"
-  patrons: "Firefish 赞助者"
-  patronsList: 按时间顺序而不是捐赠金额排列。通过上面的链接捐款,让您的名字出现在这里!
-  sponsors: Firefish 赞助者们
   donateTitle: 喜欢 Firefish 吗?
   pleaseDonateToFirefish: 请考虑赞助 Firefish 以支持其开发。
   pleaseDonateToHost: 也请考虑赞助您的主服务器 {host},以帮助支持其运营成本。
diff --git a/locales/zh-TW.yml b/locales/zh-TW.yml
index 367449350d..5a722933e6 100644
--- a/locales/zh-TW.yml
+++ b/locales/zh-TW.yml
@@ -991,10 +991,6 @@ _aboutFirefish:
   source: "原始碼"
   translation: "翻譯Firefish"
   donate: "贊助Firefish"
-  morePatrons: "還有許許多多幫助我們的其他人,非常感謝你們。 🥰"
-  patrons: "贊助者"
-  patronsList: 按時間順序列出,而不是按贊助規模列出。使用上面的連結贊助,在這裡獲得顯示您名字的機會!
-  sponsors: Firefish 贊助者們
   donateTitle: 覺得 Firefish 棒嗎?
   pleaseDonateToFirefish: 請考慮向 Firefish 贊助以支持其發展。
   pleaseDonateToHost: 還請考慮捐贈給您在使用的伺服器 {host},以支援龐大的運營成本。
diff --git a/packages/backend/src/server/api/endpoints.ts b/packages/backend/src/server/api/endpoints.ts
index 9a0de00b8b..734534b3ea 100644
--- a/packages/backend/src/server/api/endpoints.ts
+++ b/packages/backend/src/server/api/endpoints.ts
@@ -286,7 +286,6 @@ import * as ep___pinnedUsers from "./endpoints/pinned-users.js";
 import * as ep___customMotd from "./endpoints/custom-motd.js";
 import * as ep___customSplashIcons from "./endpoints/custom-splash-icons.js";
 import * as ep___latestVersion from "./endpoints/latest-version.js";
-import * as ep___patrons from "./endpoints/patrons.js";
 import * as ep___release from "./endpoints/release.js";
 import * as ep___promo_read from "./endpoints/promo/read.js";
 import * as ep___requestResetPassword from "./endpoints/request-reset-password.js";
@@ -636,7 +635,6 @@ const eps = [
 	["custom-motd", ep___customMotd],
 	["custom-splash-icons", ep___customSplashIcons],
 	["latest-version", ep___latestVersion],
-	["patrons", ep___patrons],
 	["release", ep___release],
 	["promo/read", ep___promo_read],
 	["request-reset-password", ep___requestResetPassword],
diff --git a/packages/backend/src/server/api/endpoints/patrons.ts b/packages/backend/src/server/api/endpoints/patrons.ts
deleted file mode 100644
index 7da72eb81e..0000000000
--- a/packages/backend/src/server/api/endpoints/patrons.ts
+++ /dev/null
@@ -1,33 +0,0 @@
-import define from "@/server/api/define.js";
-import * as fs from "node:fs/promises";
-import { fileURLToPath } from "node:url";
-import { dirname } from "node:path";
-
-const _filename = fileURLToPath(import.meta.url);
-const _dirname = dirname(_filename);
-
-export const meta = {
-	tags: ["meta"],
-	description: "Get Firefish patrons",
-
-	requireCredential: false,
-	requireCredentialPrivateMode: false,
-} as const;
-
-export const paramDef = {
-	type: "object",
-	properties: {
-		forceUpdate: { type: "boolean", default: false },
-	},
-	required: [],
-} as const;
-
-export default define(meta, paramDef, async (ps) => {
-	const patrons = JSON.parse(
-		await fs.readFile(`${_dirname}/../../../../../../patrons.json`, "utf-8"),
-	);
-	return {
-		patrons: patrons.patrons,
-		sponsors: patrons.sponsors,
-	};
-});
diff --git a/packages/client/src/pages/user/home.vue b/packages/client/src/pages/user/home.vue
index 13846e42d4..463e5cb4a2 100644
--- a/packages/client/src/pages/user/home.vue
+++ b/packages/client/src/pages/user/home.vue
@@ -101,18 +101,6 @@
 										v-tooltip.noDelay="i18n.ts.isBot"
 										><i :class="icon('ph-robot')"></i
 									></span>
-									<span
-										v-if="
-											patrons?.includes(
-												`@${user.username}@${
-													user.host || host
-												}`,
-											)
-										"
-										v-tooltip.noDelay="i18n.ts.isPatron"
-										style="color: var(--badge)"
-										><i :class="icon('ph-hand-coins')"></i
-									></span>
 								</div>
 							</div>
 						</div>
@@ -188,18 +176,6 @@
 									v-tooltip.noDelay="i18n.ts.isBot"
 									><i :class="icon('ph-robot')"></i
 								></span>
-								<span
-									v-if="
-										patrons?.includes(
-											`@${user.username}@${
-												user.host || host
-											}`,
-										)
-									"
-									v-tooltip.noDelay="i18n.ts.isPatron"
-									style="color: var(--badge)"
-									><i :class="icon('ph-hand-coins')"></i
-								></span>
 							</div>
 						</div>
 						<div class="follow-container">
@@ -406,7 +382,6 @@ const parallaxAnimationId = ref<null | number>(null);
 const narrow = ref<null | boolean>(null);
 const rootEl = ref<null | HTMLElement>(null);
 const bannerEl = ref<null | HTMLElement>(null);
-const patrons = ref([]);
 
 const age = computed(() => {
 	return calcAge(props.user.birthday);
@@ -452,9 +427,6 @@ const timeForThem = computed(() => {
 	return "";
 });
 
-const patronsResp = await os.api("patrons");
-patrons.value = patronsResp.patrons;
-
 function parallaxLoop() {
 	parallaxAnimationId.value = window.requestAnimationFrame(parallaxLoop);
 	parallax();
diff --git a/patrons.json b/patrons.json
deleted file mode 100644
index 738083ec98..0000000000
--- a/patrons.json
+++ /dev/null
@@ -1,118 +0,0 @@
-{
-	"patrons": [
-		"@atomicpoet@firefish.social",
-		"@shoq@mastodon.social",
-		"@pikadude@erisly.social",
-		"@sage@stop.voring.me",
-		"@sky@therian.club",
-		"@panos@electricrequiem.com",
-		"@redhunt07@www.foxyhole.io",
-		"@griff@firefish.social",
-		"@cafkafk@ck.cafkafk.com",
-		"@privateger@plasmatrap.com",
-		"@effye@toot.thoughtworks.com",
-		"@Kio@kitsunes.club",
-		"@twann@tech.lgbt",
-		"@surfbum@firefish.nz",
-		"@topher@mastodon.online",
-		"@hanicef@stop.voring.me",
-		"@nmkj@calckey.jp",
-		"@unattributed@firefish.social",
-		"@cody@misskey.codingneko.com",
-		"@kate@blahaj.zone",
-		"@emtk@mkkey.net",
-		"@jovikowi@firefish.social",
-		"@padraig@firefish.social",
-		"@pancakes@cats.city",
-		"@theresmiling@firefish.social",
-		"@kristian@firefish.social",
-		"@jo@blahaj.zone",
-		"@narF@firefish.social",
-		"@AlderForrest@raining.anvil.top",
-		"@box464@firefish.social",
-		"@MariaTheMartian@firefish.social",
-		"@nisemikol@firefish.social",
-		"@smallpatatas@blahaj.zone",
-		"@bayra@stop.voring.me",
-		"@frost@wolfdo.gg",
-		"@joebiden@fuckgov.org",
-		"@nyaa@firefish.social",
-		"@Dan@firefish.social",
-		"@dana@firefish.social",
-		"@Jdreben@firefish.social",
-		"@natalie@prismst.one",
-		"@KelsonV@wandering.shop",
-		"@breakfastmtn@firefish.social",
-		"@richardazia@mastodon.social",
-		"@joestone@firefish.social",
-		"@aj@firefish.social",
-		"@zepfanman@ramblingreaders.org",
-		"@kimby@stop.voring.me",
-		"@fyrfli@fyrfli.social",
-		"@riversidebryan@firefish.lgbt",
-		"@aRubes@sloth.run",
-		"@andreasdotorg@firefish.social",
-		"@ozzy@calckey.online",
-		"@leni@windycity.style",
-		"@mhzmodels@calckey.art",
-		"@ReflexVE@firefish.social",
-		"@mark@firefish.social",
-		"@skyizwhite@himagine.club",
-		"@Uwu@firefish.social",
-		"@jGoose@firefish.social",
-		"@kunev@blewsky.social",
-		"@Simoto@electricrequiem.com",
-		"@Evoterra@firefish.social",
-		"@LauraLangdon@procial.tchncs.de",
-		"@mho@social.heise.de",
-		"@richardazia@firefish.social",
-		"@blues653@firefish.social",
-		"@rafale_blue@calc.04.si",
-		"@esm@lethallava.land",
-		"@vmstan@vmst.io",
-		"@jtbennett@noc.social",
-		"@renere@distance.blue",
-		"@theking@kitsunes.club",
-		"@toof@fedi.toofie.net",
-		"@Punko@firefish.social",
-		"@joesbrat67@firefish.social",
-		"@arth@firefish.social",
-		"@octofloofy@ck.octofloofy.ink",
-		"@pauliehedron@infosec.town",
-		"@soulthunk@lethallava.land",
-		"@bumble@ibe.social",
-		"@DarrenNevares@firefish.social",
-		"@irfan@firefish.social",
-		"@dvd@dvd.chat",
-		"@charlie2alpha@electricrequiem.com",
-		"@arndot@layer8.space",
-		"@ryan@c.ryanccn.dev",
-		"@lapastora_deprova@firefish.social",
-		"@rameez@firefish.social",
-		"@dracoling@firetribe.org",
-		"@Space6host@firefish.social",
-		"@zakalwe@plasmatrap.com",
-		"@seasicksailor@firefish.social",
-		"@geerue@firefish.social",
-		"@WXFanatic@m.ai6yr.org",
-		"@Hunkabilly@calckey.world",
-		"@samleegray@firefish.social",
-		"@schwarzewald@kodow.net",
-		"@Conatusprinciple@firefish.social",
-		"@183231bcb@firefish.lgbt",
-		"@wiase@firefish.social",
-		"@leonieke@vitaulium.nl",
-		"@soulfire@wackywolf.xyz",
-		"@elbullazul@pub.elbullazul.com",
-		"@rafale_blue@calc.04.si",
-		"@firnin@federation.network",
-		"@clement@ck.villisek.fr",
-		"@hryggrbyr@ibe.social"
-	],
-	"sponsors": [
-		"@atomicpoet@firefish.social",
-		"@unattributed@firefish.social",
-		"@jtbennett@noc.social",
-		"\nInterkosmos Link"
-	]
-}

From 008d8d8f5c3d761552d0a27953261f0a77ed9916 Mon Sep 17 00:00:00 2001
From: naskya <m@naskya.net>
Date: Sat, 13 Apr 2024 21:30:38 +0900
Subject: [PATCH 045/110] docs: update changelog

---
 docs/changelog.md | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/docs/changelog.md b/docs/changelog.md
index f0076197bc..85f13c6238 100644
--- a/docs/changelog.md
+++ b/docs/changelog.md
@@ -8,6 +8,9 @@ Critical security updates are indicated by the :warning: icon.
 ## Unreleased
 
 - Add "Media" tab to user page
+- Improve federation and rendering of mathematical expressions
+- Remove donor information from the web client
+	- See also: https://info.firefish.dev/notes/9s1n283sb10rh869
 - Fix bugs
 
 ## [v20240405](https://firefish.dev/firefish/firefish/-/merge_requests/10733/commits)

From 5ba88f3d6f005c895528399504daddbfa7cba9d0 Mon Sep 17 00:00:00 2001
From: naskya <m@naskya.net>
Date: Sat, 13 Apr 2024 21:34:20 +0900
Subject: [PATCH 046/110] v20240413

---
 docs/api-change.md        | 2 +-
 docs/changelog.md         | 2 +-
 docs/notice-for-admins.md | 2 +-
 package.json              | 2 +-
 4 files changed, 4 insertions(+), 4 deletions(-)

diff --git a/docs/api-change.md b/docs/api-change.md
index e788b3d637..f3ed584c32 100644
--- a/docs/api-change.md
+++ b/docs/api-change.md
@@ -2,7 +2,7 @@
 
 Breaking changes are indicated by the :warning: icon.
 
-## Unreleased
+## v20240413
 
 - :warning: Removed `patrons` endpoint.
 
diff --git a/docs/changelog.md b/docs/changelog.md
index 85f13c6238..a818e09835 100644
--- a/docs/changelog.md
+++ b/docs/changelog.md
@@ -5,7 +5,7 @@ Critical security updates are indicated by the :warning: icon.
 - Server administrators should check [notice-for-admins.md](./notice-for-admins.md) as well.
 - Third-party client/bot developers may want to check [api-change.md](./api-change.md) as well.
 
-## Unreleased
+## [v20240413](https://firefish.dev/firefish/firefish/-/merge_requests/10741/commits)
 
 - Add "Media" tab to user page
 - Improve federation and rendering of mathematical expressions
diff --git a/docs/notice-for-admins.md b/docs/notice-for-admins.md
index c22b1da0aa..9bff40a65c 100644
--- a/docs/notice-for-admins.md
+++ b/docs/notice-for-admins.md
@@ -2,7 +2,7 @@
 
 You can skip intermediate versions when upgrading from an old version, but please read the notices and follow the instructions for each intermediate version before [upgrading](./upgrade.md).
 
-## Unreleased
+## v20240413
 
 ### For all users
 
diff --git a/package.json b/package.json
index bff235df6e..a7bb0a5cc1 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
 	"name": "firefish",
-	"version": "20240405",
+	"version": "20240413",
 	"repository": {
 		"type": "git",
 		"url": "https://firefish.dev/firefish/firefish.git"

From 3e43819ba1d0e1d0c2db654655c2e03eee5a8294 Mon Sep 17 00:00:00 2001
From: Lhcfl <Lhcfl@outlook.com>
Date: Sat, 13 Apr 2024 23:08:46 +0800
Subject: [PATCH 047/110] rewrite MkFormDialog

---
 .../client/src/components/MkFormDialog.vue    | 195 +++++++++---------
 packages/client/src/os.ts                     |  17 +-
 packages/client/src/types/form.ts             | 123 +++++++++++
 3 files changed, 228 insertions(+), 107 deletions(-)
 create mode 100644 packages/client/src/types/form.ts

diff --git a/packages/client/src/components/MkFormDialog.vue b/packages/client/src/components/MkFormDialog.vue
index 11f1d63e29..6007ade2f2 100644
--- a/packages/client/src/components/MkFormDialog.vue
+++ b/packages/client/src/components/MkFormDialog.vue
@@ -8,7 +8,7 @@
 		@click="cancel()"
 		@ok="ok()"
 		@close="cancel()"
-		@closed="$emit('closed')"
+		@closed="emit('closed')"
 	>
 		<template #header>
 			{{ title }}
@@ -17,86 +17,84 @@
 		<MkSpacer :margin-min="20" :margin-max="32">
 			<div class="_formRoot">
 				<template
-					v-for="item in Object.keys(form).filter(
-						(item) => !form[item].hidden,
-					)"
+					v-for="[formItem, formItemName] in unHiddenForms()"
 				>
 					<FormInput
-						v-if="form[item].type === 'number'"
-						v-model="values[item]"
+						v-if="formItem.type === 'number'"
+						v-model="values[formItemName]"
 						type="number"
-						:step="form[item].step || 1"
+						:step="formItem.step || 1"
 						class="_formBlock"
 					>
 						<template #label
-							><span v-text="form[item].label || item"></span
-							><span v-if="form[item].required === false">
+							><span v-text="formItem.label || formItemName"></span
+							><span v-if="formItem.required === false">
 								({{ i18n.ts.optional }})</span
 							></template
 						>
-						<template v-if="form[item].description" #caption>{{
-							form[item].description
+						<template v-if="formItem.description" #caption>{{
+							formItem.description
 						}}</template>
 					</FormInput>
 					<FormInput
 						v-else-if="
-							form[item].type === 'string' &&
-							!form[item].multiline
+							formItem.type === 'string' &&
+							!formItem.multiline
 						"
-						v-model="values[item]"
+						v-model="values[formItemName]"
 						type="text"
 						class="_formBlock"
 					>
 						<template #label
-							><span v-text="form[item].label || item"></span
-							><span v-if="form[item].required === false">
+							><span v-text="formItem.label || formItemName"></span
+							><span v-if="formItem.required === false">
 								({{ i18n.ts.optional }})</span
 							></template
 						>
-						<template v-if="form[item].description" #caption>{{
-							form[item].description
+						<template v-if="formItem.description" #caption>{{
+							formItem.description
 						}}</template>
 					</FormInput>
 					<FormTextarea
 						v-else-if="
-							form[item].type === 'string' && form[item].multiline
+							formItem.type === 'string' && formItem.multiline
 						"
-						v-model="values[item]"
+						v-model="values[formItemName]"
 						class="_formBlock"
 					>
 						<template #label
-							><span v-text="form[item].label || item"></span
-							><span v-if="form[item].required === false">
+							><span v-text="formItem.label || formItemName"></span
+							><span v-if="formItem.required === false">
 								({{ i18n.ts.optional }})</span
 							></template
 						>
-						<template v-if="form[item].description" #caption>{{
-							form[item].description
+						<template v-if="formItem.description" #caption>{{
+							formItem.description
 						}}</template>
 					</FormTextarea>
 					<FormSwitch
-						v-else-if="form[item].type === 'boolean'"
-						v-model="values[item]"
+						v-else-if="formItem.type === 'boolean'"
+						v-model="values[formItemName]"
 						class="_formBlock"
 					>
-						<span v-text="form[item].label || item"></span>
-						<template v-if="form[item].description" #caption>{{
-							form[item].description
+						<span v-text="formItem.label || formItemName"></span>
+						<template v-if="formItem.description" #caption>{{
+							formItem.description
 						}}</template>
 					</FormSwitch>
 					<FormSelect
-						v-else-if="form[item].type === 'enum'"
-						v-model="values[item]"
+						v-else-if="formItem.type === 'enum'"
+						v-model="values[formItemName]"
 						class="_formBlock"
 					>
-						<template #label
-							><span v-text="form[item].label || item"></span
-							><span v-if="form[item].required === false">
+						<template #label>
+						<span v-text="formItem.label || formItemName"></span>
+						<span v-if="formItem.required === false">
 								({{ i18n.ts.optional }})</span
-							></template
 						>
+						</template>
 						<option
-							v-for="item in form[item].enum"
+							v-for="item in formItem.enum"
 							:key="item.value"
 							:value="item.value"
 						>
@@ -104,18 +102,18 @@
 						</option>
 					</FormSelect>
 					<FormRadios
-						v-else-if="form[item].type === 'radio'"
-						v-model="values[item]"
+						v-else-if="formItem.type === 'radio'"
+						v-model="values[formItemName]"
 						class="_formBlock"
 					>
 						<template #label
-							><span v-text="form[item].label || item"></span
-							><span v-if="form[item].required === false">
+							><span v-text="formItem.label || formItemName"></span
+							><span v-if="formItem.required === false">
 								({{ i18n.ts.optional }})</span
 							></template
 						>
 						<option
-							v-for="item in form[item].options"
+							v-for="item in formItem.options"
 							:key="item.value"
 							:value="item.value"
 						>
@@ -123,30 +121,30 @@
 						</option>
 					</FormRadios>
 					<FormRange
-						v-else-if="form[item].type === 'range'"
-						v-model="values[item]"
-						:min="form[item].min"
-						:max="form[item].max"
-						:step="form[item].step"
-						:text-converter="form[item].textConverter"
+						v-else-if="formItem.type === 'range'"
+						v-model="values[formItemName]"
+						:min="formItem.min"
+						:max="formItem.max"
+						:step="formItem.step"
+						:text-converter="formItem.textConverter"
 						class="_formBlock"
 					>
 						<template #label
-							><span v-text="form[item].label || item"></span
-							><span v-if="form[item].required === false">
+							><span v-text="formItem.label || formItemName"></span
+							><span v-if="formItem.required === false">
 								({{ i18n.ts.optional }})</span
 							></template
 						>
-						<template v-if="form[item].description" #caption>{{
-							form[item].description
+						<template v-if="formItem.description" #caption>{{
+							formItem.description
 						}}</template>
 					</FormRange>
 					<MkButton
-						v-else-if="form[item].type === 'button'"
+						v-else-if="formItem.type === 'button'"
 						class="_formBlock"
-						@click="form[item].action($event, values)"
+						@click="formItem.action($event, values)"
 					>
-						<span v-text="form[item].content || item"></span>
+						<span v-text="formItem.content || formItemName"></span>
 					</MkButton>
 				</template>
 			</div>
@@ -154,8 +152,8 @@
 	</XModalWindow>
 </template>
 
-<script lang="ts">
-import { defineComponent } from "vue";
+<script lang="ts" setup>
+import { ref } from "vue";
 import FormInput from "./form/input.vue";
 import FormTextarea from "./form/textarea.vue";
 import FormSwitch from "./form/switch.vue";
@@ -165,59 +163,50 @@ import MkButton from "./MkButton.vue";
 import FormRadios from "./form/radios.vue";
 import XModalWindow from "@/components/MkModalWindow.vue";
 import { i18n } from "@/i18n";
+import type { FormItemType } from "@/types/form";
 
-export default defineComponent({
-	components: {
-		XModalWindow,
-		FormInput,
-		FormTextarea,
-		FormSwitch,
-		FormSelect,
-		FormRange,
-		MkButton,
-		FormRadios,
-	},
+const props = defineProps<{
+	title: string;
+	form: Record<string, FormItemType>;
+}>();
 
-	props: {
-		title: {
-			type: String,
-			required: true,
+// biome-ignore lint/suspicious/noExplicitAny: To prevent overly complex types we have to use any here
+type ValueType = Record<string, any>;
+
+const emit = defineEmits<{
+	done: [
+		status: {
+			result?: Record<string, FormItemType["default"]>;
+			canceled?: true;
 		},
-		form: {
-			type: Object,
-			required: true,
-		},
-	},
+	];
+	closed: [];
+}>();
 
-	emits: ["done"],
+const values = ref<ValueType>({});
+const dialog = ref<InstanceType<typeof XModalWindow> | null>(null);
 
-	data() {
-		return {
-			values: {},
-			i18n,
-		};
-	},
+for (const item in props.form) {
+	values.value[item] = props.form[item].default ?? null;
+}
 
-	created() {
-		for (const item in this.form) {
-			this.values[item] = this.form[item].default ?? null;
-		}
-	},
+function unHiddenForms(): [FormItemType, string][] {
+	return Object.keys(props.form)
+		.filter((itemName) => !props.form[itemName].hidden)
+		.map((itemName) => [props.form[itemName], itemName]);
+}
 
-	methods: {
-		ok() {
-			this.$emit("done", {
-				result: this.values,
-			});
-			this.$refs.dialog.close();
-		},
+function ok() {
+	emit("done", {
+		result: values.value,
+	});
+	dialog.value!.close();
+}
 
-		cancel() {
-			this.$emit("done", {
-				canceled: true,
-			});
-			this.$refs.dialog.close();
-		},
-	},
-});
+function cancel() {
+	emit("done", {
+		canceled: true,
+	});
+	dialog.value!.close();
+}
 </script>
diff --git a/packages/client/src/os.ts b/packages/client/src/os.ts
index ac1227b827..b7a17097e3 100644
--- a/packages/client/src/os.ts
+++ b/packages/client/src/os.ts
@@ -13,6 +13,7 @@ import MkWaitingDialog from "@/components/MkWaitingDialog.vue";
 import { apiUrl, url } from "@/config";
 import { me } from "@/me";
 import type { MenuItem } from "@/types/menu";
+import type { FormItemType, GetFormResultType } from "@/types/form";
 
 export const pendingApiRequestsCount = ref(0);
 
@@ -611,8 +612,16 @@ export function waiting(): Promise<void> {
 	});
 }
 
-export function form(title, form) {
-	return new Promise((resolve, reject) => {
+export function form<T extends Record<string, FormItemType>>(
+	title: string,
+	form: T,
+) {
+	return new Promise<{
+		result?: {
+			[K in keyof T]: GetFormResultType<T[K]["type"]>;
+		};
+		canceled?: true;
+	}>((resolve, _reject) => {
 		popup(
 			defineAsyncComponent({
 				loader: () => import("@/components/MkFormDialog.vue"),
@@ -622,7 +631,7 @@ export function form(title, form) {
 			{ title, form },
 			{
 				done: (result) => {
-					resolve(result);
+					resolve(result as never);
 				},
 			},
 			"closed",
@@ -631,7 +640,7 @@ export function form(title, form) {
 }
 
 export async function selectUser() {
-	return new Promise((resolve, reject) => {
+	return new Promise<entities.UserDetailed>((resolve, reject) => {
 		popup(
 			defineAsyncComponent({
 				loader: () => import("@/components/MkUserSelectDialog.vue"),
diff --git a/packages/client/src/types/form.ts b/packages/client/src/types/form.ts
new file mode 100644
index 0000000000..9c34df30dc
--- /dev/null
+++ b/packages/client/src/types/form.ts
@@ -0,0 +1,123 @@
+export type BaseFormItem = {
+	hidden?: boolean;
+	label?: string;
+	description?: string;
+	required?: boolean;
+};
+
+export type FormItemTextInput = BaseFormItem & {
+	type: "string";
+	default?: string | null;
+	multiline?: false;
+};
+export type FormItemTextarea = BaseFormItem & {
+	type: "string";
+	default?: string | null;
+	multiline: true;
+};
+
+export type FormItemText = FormItemTextInput | FormItemTextarea;
+
+export type FormItemNumber = BaseFormItem & {
+	type: "number";
+	default?: number | null;
+	step?: number | null;
+};
+export type FormItemEmail = BaseFormItem & {
+	type: "email";
+	default?: string | null;
+};
+export type FormItemPassword = BaseFormItem & {
+	type: "password";
+	default?: never;
+	__result_typedef?: string;
+};
+export type FormItemUrl = BaseFormItem & {
+	type: "url";
+	default?: string | null;
+};
+export type FormItemDate = BaseFormItem & {
+	type: "date";
+	default?: Date | null;
+};
+export type FormItemTime = BaseFormItem & {
+	type: "time";
+	default?: number | Date | null;
+};
+export type FormItemSearch = BaseFormItem & {
+	type: "search";
+	default?: string | null;
+};
+export type FormItemSwitch = BaseFormItem & {
+	type: "boolean";
+	default?: boolean | null;
+};
+export type FormItemSelect = BaseFormItem & {
+	type: "enum";
+	default?: string | null;
+	enum: {
+		value: string | number | symbol | undefined;
+		label: string;
+	}[];
+};
+export type FormItemRadios = BaseFormItem & {
+	type: "radio";
+	default?: string | number | symbol | undefined | null;
+	options: {
+		label: string;
+		value: string | number | symbol | undefined;
+	}[];
+};
+export type FormItemRange = BaseFormItem & {
+	type: "range";
+	default?: number | null;
+	min: number;
+	max: number;
+	step?: number;
+	textConverter?: (value: number) => string;
+};
+export type FormItemButton = BaseFormItem & {
+	type: "button";
+	content?: string;
+	action: (event, values) => unknown;
+	default?: never;
+};
+export type FormItemObject = BaseFormItem & {
+	type: "object";
+	default: Record<string, unknown> | null;
+	hidden: true;
+};
+
+export type FormItemInputArray = [
+	FormItemTextInput,
+	FormItemNumber,
+	FormItemEmail,
+	FormItemPassword,
+	FormItemUrl,
+	FormItemDate,
+	FormItemTime,
+	FormItemSearch,
+];
+
+export type FormItemTypeArray = [
+	...FormItemInputArray,
+	FormItemTextarea,
+	FormItemSwitch,
+	FormItemSelect,
+	FormItemButton,
+	FormItemRadios,
+	FormItemRange,
+	FormItemObject,
+];
+
+export type FormItemInput = FormItemInputArray[number];
+
+export type FormItemType = FormItemTypeArray[number];
+
+export type GetFormItemByType<T extends FormItemType["type"], F extends FormItemType = FormItemType> = F extends {type: T} ? F : never;
+
+type NonUndefindAble<T> = T extends undefined ? never : T;
+
+export type GetFormResultType<T extends FormItemType["type"], I extends FormItemType = GetFormItemByType<T>> = NonUndefindAble<
+	"__result_typedef" extends keyof I ? I["__result_typedef"] : I["default"]
+>

From 16880b1231146b0c73460917a9884cf9c5455c65 Mon Sep 17 00:00:00 2001
From: Lhcfl <Lhcfl@outlook.com>
Date: Sat, 13 Apr 2024 23:08:58 +0800
Subject: [PATCH 048/110] fix types

---
 packages/client/@types/global.d.ts            |   1 +
 packages/client/src/components/MkNoteSub.vue  |  42 +++----
 .../client/src/components/MkPollEditor.vue    |   8 +-
 packages/client/src/components/MkPostForm.vue | 111 ++++++++++--------
 .../src/components/MkPostFormDialog.vue       |   7 +-
 packages/client/src/components/MkSignup.vue   |   7 +-
 packages/client/src/components/MkTagCloud.vue |   1 +
 .../src/components/MkVisibilityPicker.vue     |   2 +-
 packages/client/src/components/form/range.vue |   2 +-
 packages/client/src/pages/clip.vue            |  10 +-
 packages/client/src/store.ts                  |   8 +-
 packages/client/src/types/post-form.ts        |  12 ++
 packages/client/tsconfig.json                 |   2 +-
 packages/firefish-js/src/api.types.ts         |  16 +--
 packages/firefish-js/src/entities.ts          |   6 +-
 packages/firefish-js/src/index.ts             |   2 +
 16 files changed, 135 insertions(+), 102 deletions(-)
 create mode 100644 packages/client/src/types/post-form.ts

diff --git a/packages/client/@types/global.d.ts b/packages/client/@types/global.d.ts
index c757482900..3ac4f09b0c 100644
--- a/packages/client/@types/global.d.ts
+++ b/packages/client/@types/global.d.ts
@@ -1,3 +1,4 @@
+// biome-ignore lint/suspicious/noExplicitAny:
 type FIXME = any;
 
 declare const _LANGS_: string[][];
diff --git a/packages/client/src/components/MkNoteSub.vue b/packages/client/src/components/MkNoteSub.vue
index 08c0bf6c38..c7cf06d2e5 100644
--- a/packages/client/src/components/MkNoteSub.vue
+++ b/packages/client/src/components/MkNoteSub.vue
@@ -1,7 +1,7 @@
 <template>
 	<article
 		v-if="!muted.muted || muted.what === 'reply'"
-		:id="detailedView ? appearNote.id : null"
+		:id="detailedView ? appearNote.id : undefined"
 		ref="el"
 		v-size="{ max: [450, 500] }"
 		class="wrpstxzv"
@@ -35,10 +35,10 @@
 						:parent-id="parentId"
 						:conversation="conversation"
 						:detailed-view="detailedView"
-						@focusfooter="footerEl.focus()"
+						@focusfooter="footerEl!.focus()"
 					/>
 					<div v-if="translating || translation" class="translation">
-						<MkLoading v-if="translating" mini />
+						<MkLoading v-if="translating || translation == null" mini />
 						<div v-else class="translated">
 							<b
 								>{{
@@ -217,6 +217,7 @@ import { useNoteCapture } from "@/scripts/use-note-capture";
 import { defaultStore } from "@/store";
 import { deepClone } from "@/scripts/clone";
 import icon from "@/scripts/icon";
+import type { NoteTranslation } from "@/types/note";
 
 const router = useRouter();
 
@@ -256,12 +257,12 @@ const isRenote =
 	note.value.fileIds.length === 0 &&
 	note.value.poll == null;
 
-const el = ref<HTMLElement>();
-const footerEl = ref<HTMLElement>();
+const el = ref<HTMLElement | null>(null);
+const footerEl = ref<HTMLElement | null>(null);
 const menuButton = ref<HTMLElement>();
-const starButton = ref<InstanceType<typeof XStarButton>>();
-const renoteButton = ref<InstanceType<typeof XRenoteButton>>();
-const reactButton = ref<HTMLElement>();
+const starButton = ref<InstanceType<typeof XStarButton> | null>(null);
+const renoteButton = ref<InstanceType<typeof XRenoteButton> | null>(null);
+const reactButton = ref<HTMLElement | null>(null);
 const appearNote = computed(() =>
 	isRenote ? (note.value.renote as entities.Note) : note.value,
 );
@@ -274,7 +275,7 @@ const muted = ref(
 		defaultStore.state.mutedLangs,
 	),
 );
-const translation = ref(null);
+const translation = ref<NoteTranslation | null>(null);
 const translating = ref(false);
 const replies: entities.Note[] =
 	props.conversation
@@ -330,21 +331,21 @@ useNoteCapture({
 	isDeletedRef: isDeleted,
 });
 
-function reply(viaKeyboard = false): void {
+function reply(_viaKeyboard = false): void {
 	pleaseLogin();
 	os.post({
 		reply: appearNote.value,
-		animation: !viaKeyboard,
+		// animation: !viaKeyboard,
 	}).then(() => {
 		focus();
 	});
 }
 
-function react(viaKeyboard = false): void {
+function react(_viaKeyboard = false): void {
 	pleaseLogin();
 	blur();
 	reactionPicker.show(
-		reactButton.value,
+		reactButton.value!,
 		(reaction) => {
 			os.api("notes/reactions/create", {
 				noteId: appearNote.value.id,
@@ -388,14 +389,15 @@ function menu(viaKeyboard = false): void {
 }
 
 function onContextmenu(ev: MouseEvent): void {
-	const isLink = (el: HTMLElement) => {
+	const isLink = (el: HTMLElement | null) => {
+		if (el == null) return;
 		if (el.tagName === "A") return true;
 		if (el.parentElement) {
 			return isLink(el.parentElement);
 		}
 	};
-	if (isLink(ev.target)) return;
-	if (window.getSelection().toString() !== "") return;
+	if (isLink(ev.target as HTMLElement | null)) return;
+	if (window.getSelection()?.toString() !== "") return;
 
 	if (defaultStore.state.useReactionPickerForContextMenu) {
 		ev.preventDefault();
@@ -454,15 +456,15 @@ function onContextmenu(ev: MouseEvent): void {
 }
 
 function focus() {
-	el.value.focus();
+	el.value!.focus();
 }
 
 function blur() {
-	el.value.blur();
+	el.value!.blur();
 }
 
-function noteClick(e) {
-	if (document.getSelection().type === "Range" || !expandOnNoteClick) {
+function noteClick(e: MouseEvent) {
+	if (document.getSelection()?.type === "Range" || !expandOnNoteClick) {
 		e.stopPropagation();
 	} else {
 		router.push(notePage(props.note));
diff --git a/packages/client/src/components/MkPollEditor.vue b/packages/client/src/components/MkPollEditor.vue
index b05f80fafa..f9dfcf80a8 100644
--- a/packages/client/src/components/MkPollEditor.vue
+++ b/packages/client/src/components/MkPollEditor.vue
@@ -84,14 +84,10 @@ import { formatDateTimeString } from "@/scripts/format-time-string";
 import { addTime } from "@/scripts/time";
 import { i18n } from "@/i18n";
 import icon from "@/scripts/icon";
+import type { PollType } from "@/types/post-form";
 
 const props = defineProps<{
-	modelValue: {
-		expiresAt: string;
-		expiredAfter: number;
-		choices: string[];
-		multiple: boolean;
-	};
+	modelValue: PollType;
 }>();
 const emit = defineEmits<{
 	"update:modelValue": [
diff --git a/packages/client/src/components/MkPostForm.vue b/packages/client/src/components/MkPostForm.vue
index 2735eb55a3..0a03dbb30e 100644
--- a/packages/client/src/components/MkPostForm.vue
+++ b/packages/client/src/components/MkPostForm.vue
@@ -20,7 +20,7 @@
 				class="account _button"
 				@click="openAccountMenu"
 			>
-				<MkAvatar :user="postAccount ?? me" class="avatar" />
+				<MkAvatar :user="postAccount ?? me!" class="avatar" />
 			</button>
 			<div class="right">
 				<span
@@ -297,14 +297,22 @@
 </template>
 
 <script lang="ts" setup>
-import { computed, inject, nextTick, onMounted, ref, watch } from "vue";
+import {
+	type Ref,
+	computed,
+	inject,
+	nextTick,
+	onMounted,
+	ref,
+	watch,
+} from "vue";
 import * as mfm from "mfm-js";
 import autosize from "autosize";
 import insertTextAtCursor from "insert-text-at-cursor";
 import { length } from "stringz";
 import { toASCII } from "punycode/";
 import { acct } from "firefish-js";
-import type { entities, languages } from "firefish-js";
+import type { ApiTypes, entities, languages } from "firefish-js";
 import { throttle } from "throttle-debounce";
 import XNoteSimple from "@/components/MkNoteSimple.vue";
 import XNotePreview from "@/components/MkNotePreview.vue";
@@ -341,6 +349,7 @@ import type { MenuItem } from "@/types/menu";
 import icon from "@/scripts/icon";
 import MkVisibilityPicker from "@/components/MkVisibilityPicker.vue";
 import type { NoteVisibility } from "@/types/note";
+import type { NoteDraft, PollType } from "@/types/post-form";
 
 const modal = inject("modal");
 
@@ -353,11 +362,11 @@ const props = withDefaults(
 		specified?: entities.User;
 		initialText?: string;
 		initialVisibility?: NoteVisibility;
-		initialLanguage?: typeof languages;
+		initialLanguage?: (typeof languages)[number];
 		initialFiles?: entities.DriveFile[];
 		initialLocalOnly?: boolean;
 		initialVisibleUsers?: entities.User[];
-		initialNote?: entities.Note;
+		initialNote?: NoteDraft;
 		instant?: boolean;
 		fixed?: boolean;
 		autofocus?: boolean;
@@ -390,12 +399,7 @@ const showBigPostButton = defaultStore.state.showBigPostButton;
 const posting = ref(false);
 const text = ref(props.initialText ?? "");
 const files = ref(props.initialFiles ?? ([] as entities.DriveFile[]));
-const poll = ref<{
-	choices: string[];
-	multiple: boolean;
-	expiresAt: string | null;
-	expiredAfter: string | null;
-} | null>(null);
+const poll = ref<PollType | null>(null);
 const useCw = ref(false);
 const showPreview = ref(defaultStore.state.showPreviewByDefault);
 const cw = ref<string | null>(null);
@@ -411,12 +415,12 @@ const visibility = ref(
 			: defaultStore.state.defaultNoteVisibility),
 );
 
-const visibleUsers = ref([]);
+const visibleUsers = ref<entities.User[]>([]);
 if (props.initialVisibleUsers) {
 	props.initialVisibleUsers.forEach(pushVisibleUser);
 }
 const draghover = ref(false);
-const quoteId = ref(null);
+const quoteId = ref<string | null>(null);
 const hasNotSpecifiedMentions = ref(false);
 const recentHashtags = ref(
 	JSON.parse(localStorage.getItem("hashtags") || "[]"),
@@ -500,7 +504,9 @@ const canPost = computed((): boolean => {
 const withHashtags = computed(
 	defaultStore.makeGetterSetter("postFormWithHashtags"),
 );
-const hashtags = computed(defaultStore.makeGetterSetter("postFormHashtags"));
+const hashtags = computed(
+	defaultStore.makeGetterSetter("postFormHashtags"),
+) as Ref<string | null>;
 
 watch(text, () => {
 	checkMissingMention();
@@ -525,7 +531,7 @@ if (props.mention) {
 
 if (
 	props.reply &&
-	(props.reply.user.username !== me.username ||
+	(props.reply.user.username !== me!.username ||
 		(props.reply.user.host != null && props.reply.user.host !== host))
 ) {
 	text.value = `@${props.reply.user.username}${
@@ -545,7 +551,7 @@ if (props.reply && props.reply.text != null) {
 				: `@${x.username}@${toASCII(otherHost)}`;
 
 		// exclude me
-		if (me.username === x.username && (x.host == null || x.host === host))
+		if (me!.username === x.username && (x.host == null || x.host === host))
 			continue;
 
 		// remove duplicates
@@ -579,7 +585,7 @@ if (
 		if (props.reply.visibleUserIds) {
 			os.api("users/show", {
 				userIds: props.reply.visibleUserIds.filter(
-					(uid) => uid !== me.id && uid !== props.reply.userId,
+					(uid) => uid !== me!.id && uid !== props.reply!.userId,
 				),
 			}).then((users) => {
 				users.forEach(pushVisibleUser);
@@ -588,7 +594,7 @@ if (
 			visibility.value = "private";
 		}
 
-		if (props.reply.userId !== me.id) {
+		if (props.reply.userId !== me!.id) {
 			os.api("users/show", { userId: props.reply.userId }).then((user) => {
 				pushVisibleUser(user);
 			});
@@ -615,7 +621,7 @@ const addRe = (s: string) => {
 if (defaultStore.state.keepCw && props.reply && props.reply.cw) {
 	useCw.value = true;
 	cw.value =
-		props.reply.user.username === me.username
+		props.reply.user.username === me!.username
 			? props.reply.cw
 			: addRe(props.reply.cw);
 }
@@ -894,11 +900,14 @@ function onCompositionEnd(ev: CompositionEvent) {
 }
 
 async function onPaste(ev: ClipboardEvent) {
+	if (ev.clipboardData == null) return;
+
 	for (const { item, i } of Array.from(ev.clipboardData.items).map(
 		(item, i) => ({ item, i }),
 	)) {
 		if (item.kind === "file") {
 			const file = item.getAsFile();
+			if (file == null) continue;
 			const lio = file.name.lastIndexOf(".");
 			const ext = lio >= 0 ? file.name.slice(lio) : "";
 			const formatted = `${formatTimeString(
@@ -911,7 +920,7 @@ async function onPaste(ev: ClipboardEvent) {
 
 	const paste = ev.clipboardData?.getData("text") ?? "";
 
-	if (!props.renote && !quoteId.value && paste.startsWith(url + "/notes/")) {
+	if (!props.renote && !quoteId.value && paste.startsWith(`${url}/notes/`)) {
 		ev.preventDefault();
 
 		os.yesno({
@@ -919,13 +928,13 @@ async function onPaste(ev: ClipboardEvent) {
 			text: i18n.ts.quoteQuestion,
 		}).then(({ canceled }) => {
 			if (canceled) {
-				insertTextAtCursor(textareaEl.value, paste);
+				insertTextAtCursor(textareaEl.value!, paste);
 				return;
 			}
 
 			quoteId.value = paste
 				.substring(url.length)
-				.match(/^\/notes\/(.+?)\/?$/)[1];
+				.match(/^\/notes\/(.+?)\/?$/)![1];
 		});
 	}
 }
@@ -956,16 +965,17 @@ function onDragover(ev) {
 	}
 }
 
-function onDragenter(ev) {
+function onDragenter(_ev) {
 	draghover.value = true;
 }
 
-function onDragleave(ev) {
+function onDragleave(_ev) {
 	draghover.value = false;
 }
 
-function onDrop(ev): void {
+function onDrop(ev: DragEvent): void {
 	draghover.value = false;
+	if (ev.dataTransfer == null) return;
 
 	// ファイルだったら
 	if (ev.dataTransfer.files.length > 0) {
@@ -1064,7 +1074,7 @@ async function post() {
 
 	const processedText = preprocess(text.value);
 
-	let postData = {
+	let postData: ApiTypes.NoteSubmitReq = {
 		editId: props.editId ? props.editId : undefined,
 		text: processedText === "" ? undefined : processedText,
 		fileIds: files.value.length > 0 ? files.value.map((f) => f.id) : undefined,
@@ -1092,7 +1102,7 @@ async function post() {
 		const hashtags_ = hashtags.value
 			.trim()
 			.split(" ")
-			.map((x) => (x.startsWith("#") ? x : "#" + x))
+			.map((x) => (x.startsWith("#") ? x : `#${x}`))
 			.join(" ");
 		postData.text = postData.text ? `${postData.text} ${hashtags_}` : hashtags_;
 	}
@@ -1104,11 +1114,11 @@ async function post() {
 		}
 	}
 
-	let token;
+	let token: string | undefined;
 
 	if (postAccount.value) {
 		const storedAccounts = await getAccounts();
-		token = storedAccounts.find((x) => x.id === postAccount.value.id)?.token;
+		token = storedAccounts.find((x) => x.id === postAccount.value!.id)?.token;
 	}
 
 	posting.value = true;
@@ -1119,10 +1129,11 @@ async function post() {
 				deleteDraft();
 				emit("posted");
 				if (postData.text && postData.text !== "") {
-					const hashtags_ = mfm
-						.parse(postData.text)
-						.filter((x) => x.type === "hashtag")
-						.map((x) => x.props.hashtag);
+					const hashtags_ = (
+						mfm
+							.parse(postData.text)
+							.filter((x) => x.type === "hashtag") as mfm.MfmHashtag[]
+					).map((x) => x.props.hashtag);
 					const history = JSON.parse(
 						localStorage.getItem("hashtags") || "[]",
 					) as string[];
@@ -1133,7 +1144,7 @@ async function post() {
 				}
 				posting.value = false;
 				postAccount.value = null;
-				nextTick(() => autosize.update(textareaEl.value));
+				nextTick(() => autosize.update(textareaEl.value!));
 			});
 		})
 		.catch((err: { message: string; id: string }) => {
@@ -1169,19 +1180,23 @@ function cancel() {
 
 function insertMention() {
 	os.selectUser().then((user) => {
-		insertTextAtCursor(textareaEl.value, "@" + acct.toString(user) + " ");
+		insertTextAtCursor(textareaEl.value!, `@${acct.toString(user)} `);
 	});
 }
 
 async function insertEmoji(ev: MouseEvent) {
-	os.openEmojiPicker(ev.currentTarget ?? ev.target, {}, textareaEl.value);
+	os.openEmojiPicker(
+		(ev.currentTarget ?? ev.target) as HTMLElement,
+		{},
+		textareaEl.value,
+	);
 }
 
 async function openCheatSheet(ev: MouseEvent) {
 	os.popup(XCheatSheet, {}, {}, "closed");
 }
 
-function showActions(ev) {
+function showActions(ev: MouseEvent) {
 	os.popupMenu(
 		postFormActions.map((action) => ({
 			text: action.title,
@@ -1198,7 +1213,7 @@ function showActions(ev) {
 				);
 			},
 		})),
-		ev.currentTarget ?? ev.target,
+		(ev.currentTarget ?? ev.target) as HTMLElement,
 	);
 }
 
@@ -1209,9 +1224,9 @@ function openAccountMenu(ev: MouseEvent) {
 		{
 			withExtraOperation: false,
 			includeCurrentAccount: true,
-			active: postAccount.value != null ? postAccount.value.id : me.id,
+			active: postAccount.value != null ? postAccount.value.id : me!.id,
 			onChoose: (account) => {
-				if (account.id === me.id) {
+				if (account.id === me!.id) {
 					postAccount.value = null;
 				} else {
 					postAccount.value = account;
@@ -1232,14 +1247,14 @@ onMounted(() => {
 	}
 
 	// TODO: detach when unmount
-	new Autocomplete(textareaEl.value, text);
-	new Autocomplete(cwInputEl.value, cw);
-	new Autocomplete(hashtagsInputEl.value, hashtags);
+	new Autocomplete(textareaEl.value!, text);
+	new Autocomplete(cwInputEl.value!, cw as Ref<string>);
+	new Autocomplete(hashtagsInputEl.value!, hashtags as Ref<string>);
 
-	autosize(textareaEl.value);
+	autosize(textareaEl.value!);
 
 	nextTick(() => {
-		autosize(textareaEl.value);
+		autosize(textareaEl.value!);
 		// 書きかけの投稿を復元
 		if (!props.instant && !props.mention && !props.specified) {
 			const draft = JSON.parse(localStorage.getItem("drafts") || "{}")[
@@ -1275,8 +1290,8 @@ onMounted(() => {
 				};
 			}
 			visibility.value = init.visibility;
-			localOnly.value = init.localOnly;
-			language.value = init.lang;
+			localOnly.value = init.localOnly ?? false;
+			language.value = init.lang ?? null;
 			quoteId.value = init.renote ? init.renote.id : null;
 		}
 
@@ -1289,7 +1304,7 @@ onMounted(() => {
 		}
 
 		nextTick(() => watchForDraft());
-		nextTick(() => autosize.update(textareaEl.value));
+		nextTick(() => autosize.update(textareaEl.value!));
 	});
 });
 </script>
diff --git a/packages/client/src/components/MkPostFormDialog.vue b/packages/client/src/components/MkPostFormDialog.vue
index cf6f45e5ed..fafb4afdd4 100644
--- a/packages/client/src/components/MkPostFormDialog.vue
+++ b/packages/client/src/components/MkPostFormDialog.vue
@@ -25,6 +25,7 @@ import type { entities, languages } from "firefish-js";
 import MkModal from "@/components/MkModal.vue";
 import MkPostForm from "@/components/MkPostForm.vue";
 import type { NoteVisibility } from "@/types/note";
+import type { NoteDraft } from "@/types/post-form";
 
 const props = defineProps<{
 	reply?: entities.Note;
@@ -34,11 +35,11 @@ const props = defineProps<{
 	specified?: entities.User;
 	initialText?: string;
 	initialVisibility?: NoteVisibility;
-	initialLanguage?: typeof languages;
+	initialLanguage?: (typeof languages)[number];
 	initialFiles?: entities.DriveFile[];
 	initialLocalOnly?: boolean;
 	initialVisibleUsers?: entities.User[];
-	initialNote?: entities.Note;
+	initialNote?: NoteDraft;
 	instant?: boolean;
 	fixed?: boolean;
 	autofocus?: boolean;
@@ -53,7 +54,7 @@ const modal = shallowRef<InstanceType<typeof MkModal>>();
 const form = shallowRef<InstanceType<typeof MkPostForm>>();
 
 function onPosted() {
-	modal.value.close({
+	modal.value!.close({
 		useSendAnimation: true,
 	});
 }
diff --git a/packages/client/src/components/MkSignup.vue b/packages/client/src/components/MkSignup.vue
index 9870263b7a..0807935be3 100644
--- a/packages/client/src/components/MkSignup.vue
+++ b/packages/client/src/components/MkSignup.vue
@@ -319,7 +319,7 @@ const usernameState = ref<
 	| "invalid-format"
 	| "min-range"
 	| "max-range"
-	>(null);
+>(null);
 const invitationState = ref<null | "entered">(null);
 const emailState = ref<
 	| null
@@ -331,9 +331,10 @@ const emailState = ref<
 	| "unavailable:mx"
 	| "unavailable:smtp"
 	| "unavailable"
-	| "error">(null);
+	| "error"
+>(null);
 const passwordStrength = ref<"" | "low" | "medium" | "high">("");
-const passwordRetypeState = ref<null | "match" | "not-match" >(null);
+const passwordRetypeState = ref<null | "match" | "not-match">(null);
 const submitting = ref(false);
 const ToSAgreement = ref(false);
 const hCaptchaResponse = ref(null);
diff --git a/packages/client/src/components/MkTagCloud.vue b/packages/client/src/components/MkTagCloud.vue
index 40dee3c576..d932cbdc8c 100644
--- a/packages/client/src/components/MkTagCloud.vue
+++ b/packages/client/src/components/MkTagCloud.vue
@@ -76,6 +76,7 @@ onMounted(() => {
 					src: "/client-assets/tagcanvas.min.js",
 				}),
 			)
+			// biome-ignore lint/suspicious/noAssignInExpressions: assign it intentially
 			.addEventListener("load", () => (available.value = true));
 	}
 });
diff --git a/packages/client/src/components/MkVisibilityPicker.vue b/packages/client/src/components/MkVisibilityPicker.vue
index 088da07eb0..bfa8102615 100644
--- a/packages/client/src/components/MkVisibilityPicker.vue
+++ b/packages/client/src/components/MkVisibilityPicker.vue
@@ -153,7 +153,7 @@ const props = withDefaults(
 	defineProps<{
 		currentVisibility: NoteVisibility;
 		currentLocalOnly: boolean;
-		src?: HTMLElement;
+		src?: HTMLElement | null;
 	}>(),
 	{},
 );
diff --git a/packages/client/src/components/form/range.vue b/packages/client/src/components/form/range.vue
index d12596a86c..1377abf034 100644
--- a/packages/client/src/components/form/range.vue
+++ b/packages/client/src/components/form/range.vue
@@ -48,7 +48,7 @@ const id = os.getUniqueId();
 
 const props = withDefaults(
 	defineProps<{
-		modelValue: number;
+		modelValue: number | null;
 		disabled?: boolean;
 		min: number;
 		max: number;
diff --git a/packages/client/src/pages/clip.vue b/packages/client/src/pages/clip.vue
index ffe00beeec..8507721519 100644
--- a/packages/client/src/pages/clip.vue
+++ b/packages/client/src/pages/clip.vue
@@ -75,29 +75,29 @@ const headerActions = computed(() =>
 					icon: `${icon("ph-pencil")}`,
 					text: i18n.ts.toEdit,
 					handler: async (): Promise<void> => {
-						const { canceled, result } = await os.form(clip.value.name, {
+						const { canceled, result } = await os.form(clip.value!.name, {
 							name: {
 								type: "string",
 								label: i18n.ts.name,
-								default: clip.value.name,
+								default: clip.value!.name,
 							},
 							description: {
 								type: "string",
 								required: false,
 								multiline: true,
 								label: i18n.ts.description,
-								default: clip.value.description,
+								default: clip.value!.description,
 							},
 							isPublic: {
 								type: "boolean",
 								label: i18n.ts.public,
-								default: clip.value.isPublic,
+								default: clip.value!.isPublic,
 							},
 						});
 						if (canceled) return;
 
 						os.apiWithDialog("clips/update", {
-							clipId: clip.value.id,
+							clipId: clip.value!.id,
 							...result,
 						});
 					},
diff --git a/packages/client/src/store.ts b/packages/client/src/store.ts
index 85224f0d86..680dba286f 100644
--- a/packages/client/src/store.ts
+++ b/packages/client/src/store.ts
@@ -2,14 +2,15 @@ import { markRaw, ref } from "vue";
 import { isSignedIn } from "./me";
 import { Storage } from "./pizzax";
 import type { NoteVisibility } from "@/types/note";
+import type { entities, ApiTypes } from "firefish-js";
 
 export const postFormActions: {
 	title: string;
-	handler: (note: entities.Note) => void | Promise<void>;
+	handler: (from, update) => void | Promise<void>;
 }[] = [];
 export const userActions: {
 	title: string;
-	handler: (note: entities.Note) => void | Promise<void>;
+	handler: (user: entities.User) => void | Promise<void>;
 }[] = [];
 export const noteActions: {
 	title: string;
@@ -19,7 +20,7 @@ export const noteViewInterruptors: {
 	handler: (note: entities.Note) => Promise<entities.Note>;
 }[] = [];
 export const notePostInterruptors: {
-	handler: (note: entities.Note) => Promise<entities.Note>;
+	handler: (note: ApiTypes.NoteSubmitReq) => Promise<ApiTypes.NoteSubmitReq>;
 }[] = [];
 
 const menuOptions = [
@@ -466,7 +467,6 @@ import darkTheme from "@/themes/d-rosepine.json5";
  * Storage for configuration information that does not need to be constantly loaded into memory (non-reactive)
  */
 import lightTheme from "@/themes/l-rosepinedawn.json5";
-import { entities } from "firefish-js";
 
 export class ColdDeviceStorage {
 	public static default = {
diff --git a/packages/client/src/types/post-form.ts b/packages/client/src/types/post-form.ts
new file mode 100644
index 0000000000..a535ef812f
--- /dev/null
+++ b/packages/client/src/types/post-form.ts
@@ -0,0 +1,12 @@
+import type { entities } from "firefish-js";
+
+export type PollType = {
+	choices: string[];
+	multiple: boolean;
+	expiresAt: string | null;
+	expiredAfter: number | null;
+};
+
+export type NoteDraft = entities.Note & {
+	poll?: PollType;
+};
diff --git a/packages/client/tsconfig.json b/packages/client/tsconfig.json
index 4d62c784dd..51f6e8dd1e 100644
--- a/packages/client/tsconfig.json
+++ b/packages/client/tsconfig.json
@@ -30,5 +30,5 @@
 		"jsx": "preserve"
 	},
 	"compileOnSave": false,
-	"include": ["./src/**/*.ts", "./src/**/*.vue"]
+	"include": ["./src/**/*.ts", "./src/**/*.vue", "./@types"]
 }
diff --git a/packages/firefish-js/src/api.types.ts b/packages/firefish-js/src/api.types.ts
index ffb22c191b..30ae1f9cdf 100644
--- a/packages/firefish-js/src/api.types.ts
+++ b/packages/firefish-js/src/api.types.ts
@@ -36,7 +36,6 @@ import type {
 	UserDetailed,
 	UserGroup,
 	UserList,
-	UserLite,
 	UserSorting,
 } from "./entities";
 
@@ -46,11 +45,13 @@ type TODO = Record<string, any> | null;
 
 type NoParams = Record<string, never>;
 
-type ShowUserReq = { username: string; host?: string } | { userId: User["id"] };
+type ShowUserReq =
+	| { username: string; host?: string | null }
+	| { userId: User["id"] };
 
-type NoteSubmitReq = {
+export type NoteSubmitReq = {
 	editId?: null | Note["id"];
-	visibility?: "public" | "home" | "followers" | "specified";
+	visibility?: (typeof consts.noteVisibilities)[number];
 	visibleUserIds?: User["id"][];
 	text?: null | string;
 	cw?: null | string;
@@ -62,10 +63,11 @@ type NoteSubmitReq = {
 	channelId?: null | Channel["id"];
 	poll?: null | {
 		choices: string[];
-		multiple?: boolean;
-		expiresAt?: null | number;
-		expiredAfter?: null | number;
+		multiple: boolean;
+		expiresAt: string | null;
+		expiredAfter: number | null;
 	};
+	lang?: string;
 };
 
 export type Endpoints = {
diff --git a/packages/firefish-js/src/entities.ts b/packages/firefish-js/src/entities.ts
index 5858dacb43..457d7ac935 100644
--- a/packages/firefish-js/src/entities.ts
+++ b/packages/firefish-js/src/entities.ts
@@ -29,7 +29,7 @@ export type UserLite = {
 	isIndexable: boolean;
 	isCat?: boolean;
 	speakAsCat?: boolean;
-	driveCapacityOverrideMb: number | null,
+	driveCapacityOverrideMb: number | null;
 };
 
 export type UserDetailed = UserLite & {
@@ -238,8 +238,8 @@ export interface RenoteNotification extends BaseNotification {
 	user: User;
 	userId: User["id"];
 	note: Note & {
-		renote: Note,
-		renoteId: string,
+		renote: Note;
+		renoteId: string;
 	};
 }
 export interface QuoteNotification extends BaseNotification {
diff --git a/packages/firefish-js/src/index.ts b/packages/firefish-js/src/index.ts
index 6639985481..3398ed8a2e 100644
--- a/packages/firefish-js/src/index.ts
+++ b/packages/firefish-js/src/index.ts
@@ -1,6 +1,7 @@
 import * as acct from "./acct";
 import type { Acct } from "./acct";
 import { Endpoints } from "./api.types";
+import type * as ApiTypes from "./api.types";
 import * as consts from "./consts";
 import Stream, { Connection } from "./streaming";
 import * as StreamTypes from "./streaming.types";
@@ -8,6 +9,7 @@ import type * as TypeUtils from "./type-utils";
 
 export {
 	Endpoints,
+	type ApiTypes,
 	Stream,
 	Connection as ChannelConnection,
 	StreamTypes,

From 1a6ba246f22b4d430db8d105fe8ea4c4db3dfc91 Mon Sep 17 00:00:00 2001
From: Lhcfl <Lhcfl@outlook.com>
Date: Sat, 13 Apr 2024 23:21:31 +0800
Subject: [PATCH 049/110] add other FormInput

---
 .../client/src/components/MkFormDialog.vue    | 23 +++++++++++++++++++
 1 file changed, 23 insertions(+)

diff --git a/packages/client/src/components/MkFormDialog.vue b/packages/client/src/components/MkFormDialog.vue
index 6007ade2f2..b3d02ef515 100644
--- a/packages/client/src/components/MkFormDialog.vue
+++ b/packages/client/src/components/MkFormDialog.vue
@@ -55,6 +55,29 @@
 							formItem.description
 						}}</template>
 					</FormInput>
+					<FormInput
+						v-else-if="
+							formItem.type === 'email' ||
+							formItem.type === 'password' ||
+							formItem.type === 'url' ||
+							formItem.type === 'date' ||
+							formItem.type === 'time' ||
+							formItem.type === 'search'
+						"
+						v-model="values[formItemName]"
+						:type="formItem.type"
+						class="_formBlock"
+					>
+						<template #label
+							><span v-text="formItem.label || formItemName"></span
+							><span v-if="formItem.required === false">
+								({{ i18n.ts.optional }})</span
+							></template
+						>
+						<template v-if="formItem.description" #caption>{{
+							formItem.description
+						}}</template>
+					</FormInput>
 					<FormTextarea
 						v-else-if="
 							formItem.type === 'string' && formItem.multiline

From aea6659d0bee60b5930155fc3e9eda7cd167b3c8 Mon Sep 17 00:00:00 2001
From: naskya <m@naskya.net>
Date: Sun, 14 Apr 2024 02:35:25 +0900
Subject: [PATCH 050/110] fix: workaround the issue that profile pages don't
 load if the version is older than 20240212

(v20240212 is vulnerable btw)
---
 patrons.json | 4 ++++
 1 file changed, 4 insertions(+)
 create mode 100644 patrons.json

diff --git a/patrons.json b/patrons.json
new file mode 100644
index 0000000000..cbbeacfdc9
--- /dev/null
+++ b/patrons.json
@@ -0,0 +1,4 @@
+{
+  "patrons": [],
+  "sponsors": []
+}

From cee3a13f510b914f0c67392941739f3a0e53803b Mon Sep 17 00:00:00 2001
From: Lhcfl <Lhcfl@outlook.com>
Date: Sun, 14 Apr 2024 03:15:31 +0800
Subject: [PATCH 051/110] fix types

---
 packages/client/@types/window.d.ts            |   6 +
 packages/client/src/components/MkDialog.vue   |  37 ++--
 packages/client/src/components/MkMenu.vue     |   3 +-
 .../client/src/components/MkUrlPreview.vue    |   4 +-
 .../client/src/components/MkWaitingDialog.vue |   8 +-
 packages/client/src/os.ts                     | 164 +++++++++++-------
 .../page-editor/els/page-editor.el.image.vue  |   4 +-
 .../pages/settings/preferences-backups.vue    |   4 +-
 .../client/src/pages/user-list-timeline.vue   |   4 +-
 packages/client/src/scripts/form.ts           |  73 +-------
 packages/client/src/scripts/get-note-menu.ts  |   7 +-
 packages/client/src/scripts/select-file.ts    |  35 ++--
 packages/client/src/types/form.ts             |  29 +++-
 packages/client/src/widgets/widget.ts         |   4 +-
 packages/firefish-js/src/api.ts               |   2 +-
 packages/firefish-js/src/api.types.ts         |   1 +
 16 files changed, 194 insertions(+), 191 deletions(-)
 create mode 100644 packages/client/@types/window.d.ts

diff --git a/packages/client/@types/window.d.ts b/packages/client/@types/window.d.ts
new file mode 100644
index 0000000000..1ae20c0d20
--- /dev/null
+++ b/packages/client/@types/window.d.ts
@@ -0,0 +1,6 @@
+declare global {
+	interface Window {
+		__misskey_input_ref__?: HTMLInputElement | null;
+	}
+}
+export type {};
diff --git a/packages/client/src/components/MkDialog.vue b/packages/client/src/components/MkDialog.vue
index c1f8f581a7..a345f7dcea 100644
--- a/packages/client/src/components/MkDialog.vue
+++ b/packages/client/src/components/MkDialog.vue
@@ -216,25 +216,32 @@ interface Input {
 		| "paragraph";
 	placeholder?: string | null;
 	autocomplete?: string;
-	default: string | number | null;
+	default?: string | number | null;
 	minLength?: number;
 	maxLength?: number;
 }
 
-interface Select {
-	items: {
-		value: string;
-		text: string;
-	}[];
-	groupedItems: {
-		label: string;
-		items: {
-			value: string;
-			text: string;
-		}[];
-	}[];
-	default: string | null;
-}
+type Select = {
+	default?: string | null;
+} & (
+	| {
+			items: {
+				value: string;
+				text: string;
+			}[];
+			groupedItems?: undefined;
+	  }
+	| {
+			items?: undefined;
+			groupedItems: {
+				label: string;
+				items: {
+					value: string;
+					text: string;
+				}[];
+			}[];
+	  }
+);
 
 const props = withDefaults(
 	defineProps<{
diff --git a/packages/client/src/components/MkMenu.vue b/packages/client/src/components/MkMenu.vue
index c778f0f6f1..e92305d8f3 100644
--- a/packages/client/src/components/MkMenu.vue
+++ b/packages/client/src/components/MkMenu.vue
@@ -254,8 +254,7 @@ const isActive = ref();
 watch(
 	() => props.items,
 	() => {
-		// FIXME: what's this?
-		const items: (MenuItem | MenuPending)[] = [...props.items].filter(
+		const items: (MenuItem | MenuPending)[] = props.items.filter(
 			(item) => item !== undefined,
 		);
 
diff --git a/packages/client/src/components/MkUrlPreview.vue b/packages/client/src/components/MkUrlPreview.vue
index e72bdd704e..95e46ee2cc 100644
--- a/packages/client/src/components/MkUrlPreview.vue
+++ b/packages/client/src/components/MkUrlPreview.vue
@@ -181,10 +181,10 @@ function adjustTweetHeight(message: any) {
 	if (height) tweetHeight.value = height;
 }
 
-(window as any).addEventListener("message", adjustTweetHeight);
+window.addEventListener("message", adjustTweetHeight);
 
 onUnmounted(() => {
-	(window as any).removeEventListener("message", adjustTweetHeight);
+	window.removeEventListener("message", adjustTweetHeight);
 });
 </script>
 
diff --git a/packages/client/src/components/MkWaitingDialog.vue b/packages/client/src/components/MkWaitingDialog.vue
index c35023adcb..18cec42a49 100644
--- a/packages/client/src/components/MkWaitingDialog.vue
+++ b/packages/client/src/components/MkWaitingDialog.vue
@@ -13,7 +13,7 @@
 			]"
 		>
 			<i
-				v-if="success"
+				v-if="unref(success)"
 				:class="[$style.icon, $style.success, iconify('ph-check')]"
 			></i>
 			<MkLoading
@@ -29,15 +29,15 @@
 </template>
 
 <script lang="ts" setup>
-import { shallowRef, watch } from "vue";
+import { MaybeRef, shallowRef, watch, unref } from "vue";
 import MkModal from "@/components/MkModal.vue";
 import iconify from "@/scripts/icon";
 
 const modal = shallowRef<InstanceType<typeof MkModal>>();
 
 const props = defineProps<{
-	success: boolean;
-	showing: boolean;
+	success: MaybeRef<boolean>;
+	showing: MaybeRef<boolean>;
 	text?: string;
 }>();
 
diff --git a/packages/client/src/os.ts b/packages/client/src/os.ts
index b7a17097e3..0bdacd3adb 100644
--- a/packages/client/src/os.ts
+++ b/packages/client/src/os.ts
@@ -1,7 +1,7 @@
 // TODO: なんでもかんでもos.tsに突っ込むのやめたいのでよしなに分割する
 
 import { EventEmitter } from "eventemitter3";
-import { type entities, api as firefishApi } from "firefish-js";
+import { type entities, api as firefishApi, type Endpoints } from "firefish-js";
 import insertTextAtCursor from "insert-text-at-cursor";
 import type { Component, Ref } from "vue";
 import { defineAsyncComponent, markRaw, ref } from "vue";
@@ -13,7 +13,7 @@ import MkWaitingDialog from "@/components/MkWaitingDialog.vue";
 import { apiUrl, url } from "@/config";
 import { me } from "@/me";
 import type { MenuItem } from "@/types/menu";
-import type { FormItemType, GetFormResultType } from "@/types/form";
+import type { Form, GetFormResultType } from "@/types/form";
 
 export const pendingApiRequestsCount = ref(0);
 
@@ -52,7 +52,7 @@ export const api = ((
 				if (res.status === 200) {
 					resolve(body);
 				} else if (res.status === 204) {
-					resolve();
+					resolve(undefined);
 				} else {
 					reject(body.error);
 				}
@@ -67,7 +67,7 @@ export const api = ((
 
 export const apiGet = ((
 	endpoint: string,
-	data: Record<string, any> = {},
+	data: URLSearchParams | string | string[][] | Record<string, string> = {},
 	token?: string | null | undefined,
 ) => {
 	pendingApiRequestsCount.value++;
@@ -97,7 +97,7 @@ export const apiGet = ((
 				if (res.status === 200) {
 					resolve(body);
 				} else if (res.status === 204) {
-					resolve();
+					resolve(undefined);
 				} else {
 					reject(body.error);
 				}
@@ -111,15 +111,15 @@ export const apiGet = ((
 }) as typeof apiClient.request;
 
 export const apiWithDialog = ((
-	endpoint: string,
-	data: Record<string, any> = {},
+	endpoint: keyof Endpoints,
+	data: Record<string, unknown> = {},
 	token?: string | null | undefined,
 ) => {
 	const promise = api(endpoint, data, token);
 	promiseDialog(promise, null, (err) => {
 		alert({
 			type: "error",
-			text: err.message + "\n" + (err as any).id,
+			text: `${err.message}\n${err.id}`,
 		});
 	});
 
@@ -129,7 +129,7 @@ export const apiWithDialog = ((
 export function promiseDialog<T>(
 	promise: Promise<T>,
 	onSuccess?: ((res: T) => void) | null,
-	onFailure?: ((err: Error) => void) | null,
+	onFailure?: ((err: firefishApi.APIError) => void) | null,
 	text?: string,
 ): Promise<T> {
 	const showing = ref(true);
@@ -294,7 +294,7 @@ export function alert(props: {
 	text?: string | null;
 	isPlaintext?: boolean;
 }): Promise<void> {
-	return new Promise((resolve, reject) => {
+	return new Promise((resolve, _reject) => {
 		if (props.text == null && props.type === "error") {
 			props.text = i18n.ts.somethingHappened;
 		}
@@ -319,7 +319,7 @@ export function confirm(props: {
 	cancelText?: string;
 	isPlaintext?: boolean;
 }): Promise<{ canceled: boolean }> {
-	return new Promise((resolve, reject) => {
+	return new Promise((resolve, _reject) => {
 		popup(
 			MkDialog,
 			{
@@ -342,7 +342,7 @@ export function yesno(props: {
 	text?: string | null;
 	isPlaintext?: boolean;
 }): Promise<{ canceled: boolean }> {
-	return new Promise((resolve, reject) => {
+	return new Promise((resolve, _reject) => {
 		popup(
 			defineAsyncComponent({
 				loader: () => import("@/components/MkDialog.vue"),
@@ -374,13 +374,13 @@ export function inputText(props: {
 	minLength?: number;
 	maxLength?: number;
 }): Promise<
-	| { canceled: true; result: undefined }
+	| { canceled: true; result?: undefined }
 	| {
 			canceled: false;
 			result: string;
 	  }
 > {
-	return new Promise((resolve, reject) => {
+	return new Promise((resolve, _reject) => {
 		popup(
 			MkDialog,
 			{
@@ -397,7 +397,14 @@ export function inputText(props: {
 			},
 			{
 				done: (result) => {
-					resolve(result || { canceled: true });
+					if (result.canceled) {
+						resolve({ canceled: true });
+					} else {
+						resolve({
+							canceled: false,
+							result: String(result.result),
+						});
+					}
 				},
 			},
 			"closed",
@@ -411,13 +418,13 @@ export function inputParagraph(props: {
 	placeholder?: string | null;
 	default?: string | null;
 }): Promise<
-	| { canceled: true; result: undefined }
+	| { canceled: true; result?: undefined }
 	| {
 			canceled: false;
 			result: string;
 	  }
 > {
-	return new Promise((resolve, reject) => {
+	return new Promise((resolve, _reject) => {
 		popup(
 			defineAsyncComponent({
 				loader: () => import("@/components/MkDialog.vue"),
@@ -435,7 +442,14 @@ export function inputParagraph(props: {
 			},
 			{
 				done: (result) => {
-					resolve(result || { canceled: true });
+					if (result.canceled) {
+						resolve({ canceled: true });
+					} else {
+						resolve({
+							canceled: false,
+							result: String(result.result),
+						});
+					}
 				},
 			},
 			"closed",
@@ -450,13 +464,13 @@ export function inputNumber(props: {
 	default?: number | null;
 	autocomplete?: string;
 }): Promise<
-	| { canceled: true; result: undefined }
+	| { canceled: true; result?: undefined }
 	| {
 			canceled: false;
 			result: number;
 	  }
 > {
-	return new Promise((resolve, reject) => {
+	return new Promise((resolve, _reject) => {
 		popup(
 			defineAsyncComponent({
 				loader: () => import("@/components/MkDialog.vue"),
@@ -475,7 +489,14 @@ export function inputNumber(props: {
 			},
 			{
 				done: (result) => {
-					resolve(result || { canceled: true });
+					if (result.canceled) {
+						resolve({ canceled: true });
+					} else {
+						resolve({
+							canceled: false,
+							result: Number(result.result),
+						});
+					}
 				},
 			},
 			"closed",
@@ -487,15 +508,16 @@ export function inputDate(props: {
 	title?: string | null;
 	text?: string | null;
 	placeholder?: string | null;
-	default?: Date | null;
+	default?: Date | string | null;
 }): Promise<
-	| { canceled: true; result: undefined }
+	| { canceled: true; result?: undefined }
 	| {
 			canceled: false;
 			result: Date;
 	  }
 > {
-	return new Promise((resolve, reject) => {
+	props.default ??= new Date();
+	return new Promise((resolve, _reject) => {
 		popup(
 			MkDialog,
 			{
@@ -504,7 +526,10 @@ export function inputDate(props: {
 				input: {
 					type: "date",
 					placeholder: props.placeholder,
-					default: props.default,
+					default:
+						props.default instanceof Date
+							? props.default.toISOString().slice(0, 10)
+							: props.default,
 				},
 			},
 			{
@@ -512,7 +537,7 @@ export function inputDate(props: {
 					resolve(
 						result
 							? {
-									result: new Date(result.result),
+									result: new Date(result.result as string | number),
 									canceled: false,
 								}
 							: { canceled: true },
@@ -524,7 +549,7 @@ export function inputDate(props: {
 	});
 }
 
-export function select<C = any>(
+export function select<C extends string>(
 	props: {
 		title?: string | null;
 		text?: string | null;
@@ -535,8 +560,10 @@ export function select<C = any>(
 					value: C;
 					text: string;
 				}[];
+				groupedItems?: undefined;
 		  }
 		| {
+				items?: undefined;
 				groupedItems: {
 					label: string;
 					items: {
@@ -547,27 +574,35 @@ export function select<C = any>(
 		  }
 	),
 ): Promise<
-	| { canceled: true; result: undefined }
+	| { canceled: true; result?: undefined }
 	| {
 			canceled: false;
 			result: C;
 	  }
 > {
-	return new Promise((resolve, reject) => {
+	return new Promise((resolve, _reject) => {
 		popup(
 			MkDialog,
 			{
 				title: props.title,
 				text: props.text,
-				select: {
-					items: props.items,
-					groupedItems: props.groupedItems,
-					default: props.default,
-				},
+				select: props.items
+					? {
+							items: props.items,
+							default: props.default,
+						}
+					: {
+							groupedItems: props.groupedItems,
+							default: props.default,
+						},
 			},
 			{
 				done: (result) => {
-					resolve(result || { canceled: true });
+					if (result.canceled) {
+						resolve({ canceled: true });
+					} else {
+						resolve(result as never);
+					}
 				},
 			},
 			"closed",
@@ -576,7 +611,7 @@ export function select<C = any>(
 }
 
 export function success(): Promise<void> {
-	return new Promise((resolve, reject) => {
+	return new Promise((resolve, _reject) => {
 		const showing = ref(true);
 		window.setTimeout(() => {
 			showing.value = false;
@@ -596,7 +631,7 @@ export function success(): Promise<void> {
 }
 
 export function waiting(): Promise<void> {
-	return new Promise((resolve, reject) => {
+	return new Promise((resolve, _reject) => {
 		const showing = ref(true);
 		popup(
 			MkWaitingDialog,
@@ -612,14 +647,9 @@ export function waiting(): Promise<void> {
 	});
 }
 
-export function form<T extends Record<string, FormItemType>>(
-	title: string,
-	form: T,
-) {
+export function form<T extends Form>(title: string, form: T) {
 	return new Promise<{
-		result?: {
-			[K in keyof T]: GetFormResultType<T[K]["type"]>;
-		};
+		result?: GetFormResultType<T>;
 		canceled?: true;
 	}>((resolve, _reject) => {
 		popup(
@@ -639,8 +669,8 @@ export function form<T extends Record<string, FormItemType>>(
 	});
 }
 
-export async function selectUser() {
-	return new Promise<entities.UserDetailed>((resolve, reject) => {
+export function selectUser() {
+	return new Promise<entities.UserDetailed>((resolve, _reject) => {
 		popup(
 			defineAsyncComponent({
 				loader: () => import("@/components/MkUserSelectDialog.vue"),
@@ -658,8 +688,8 @@ export async function selectUser() {
 	});
 }
 
-export async function selectLocalUser() {
-	return new Promise((resolve, reject) => {
+export function selectLocalUser() {
+	return new Promise<entities.UserDetailed>((resolve, _reject) => {
 		popup(
 			defineAsyncComponent({
 				loader: () => import("@/components/MkUserSelectLocalDialog.vue"),
@@ -677,8 +707,8 @@ export async function selectLocalUser() {
 	});
 }
 
-export async function selectInstance(): Promise<entities.Instance> {
-	return new Promise((resolve, reject) => {
+export function selectInstance(): Promise<entities.Instance> {
+	return new Promise((resolve, _reject) => {
 		popup(
 			defineAsyncComponent({
 				loader: () => import("@/components/MkInstanceSelectDialog.vue"),
@@ -696,8 +726,10 @@ export async function selectInstance(): Promise<entities.Instance> {
 	});
 }
 
-export async function selectDriveFile(multiple: boolean) {
-	return new Promise((resolve, reject) => {
+export function selectDriveFile<Multiple extends boolean>(multiple: Multiple) {
+	return new Promise<
+		Multiple extends true ? entities.DriveFile[] : entities.DriveFile
+	>((resolve, _reject) => {
 		popup(
 			defineAsyncComponent({
 				loader: () => import("@/components/MkDriveSelectDialog.vue"),
@@ -711,7 +743,7 @@ export async function selectDriveFile(multiple: boolean) {
 			{
 				done: (files) => {
 					if (files) {
-						resolve(multiple ? files : files[0]);
+						resolve((multiple ? files : files[0]) as never);
 					}
 				},
 			},
@@ -721,7 +753,7 @@ export async function selectDriveFile(multiple: boolean) {
 }
 
 export async function selectDriveFolder(multiple: boolean) {
-	return new Promise((resolve, reject) => {
+	return new Promise((resolve, _reject) => {
 		popup(
 			defineAsyncComponent({
 				loader: () => import("@/components/MkDriveSelectDialog.vue"),
@@ -745,7 +777,7 @@ export async function selectDriveFolder(multiple: boolean) {
 }
 
 export async function pickEmoji(src: HTMLElement | null, opts) {
-	return new Promise((resolve, reject) => {
+	return new Promise((resolve, _reject) => {
 		popup(
 			defineAsyncComponent({
 				loader: () => import("@/components/MkEmojiPickerDialog.vue"),
@@ -772,7 +804,7 @@ export async function cropImage(
 		aspectRatio: number;
 	},
 ): Promise<entities.DriveFile> {
-	return new Promise((resolve, reject) => {
+	return new Promise((resolve, _reject) => {
 		popup(
 			defineAsyncComponent({
 				loader: () => import("@/components/MkCropperDialog.vue"),
@@ -795,13 +827,13 @@ export async function cropImage(
 
 type AwaitType<T> = T extends Promise<infer U>
 	? U
-	: T extends (...args: any[]) => Promise<infer V>
+	: T extends (...args: unknown[]) => Promise<infer V>
 		? V
 		: T;
 let openingEmojiPicker: AwaitType<ReturnType<typeof popup>> | null = null;
-let activeTextarea: HTMLTextAreaElement | HTMLInputElement | null = null;
+let activeTextarea: HTMLTextAreaElement | HTMLInputElement;
 export async function openEmojiPicker(
-	src?: HTMLElement,
+	src: HTMLElement | undefined,
 	opts,
 	initialTextarea: typeof activeTextarea,
 ) {
@@ -809,7 +841,7 @@ export async function openEmojiPicker(
 
 	activeTextarea = initialTextarea;
 
-	const textareas = document.querySelectorAll("textarea, input");
+	const textareas = document.querySelectorAll<HTMLTextAreaElement | HTMLInputElement>("textarea, input");
 	for (const textarea of Array.from(textareas)) {
 		textarea.addEventListener("focus", () => {
 			activeTextarea = textarea;
@@ -821,7 +853,7 @@ export async function openEmojiPicker(
 			for (const node of Array.from(record.addedNodes).filter(
 				(node) => node instanceof HTMLElement,
 			) as HTMLElement[]) {
-				const textareas = node.querySelectorAll("textarea, input");
+				const textareas = node.querySelectorAll<HTMLTextAreaElement | HTMLInputElement>("textarea, input");
 				for (const textarea of Array.from(textareas).filter(
 					(textarea) => textarea.dataset.preventEmojiInsert == null,
 				)) {
@@ -859,7 +891,7 @@ export async function openEmojiPicker(
 				insertTextAtCursor(activeTextarea, emoji);
 			},
 			closed: () => {
-				openingEmojiPicker!.dispose();
+				openingEmojiPicker?.dispose();
 				openingEmojiPicker = null;
 				observer.disconnect();
 			},
@@ -910,8 +942,8 @@ export function contextMenu(
 	ev: MouseEvent,
 ) {
 	ev.preventDefault();
-	return new Promise((resolve, reject) => {
-		let dispose;
+	return new Promise<void>((resolve, _reject) => {
+		let dispose: () => void;
 		popup(
 			defineAsyncComponent({
 				loader: () => import("@/components/MkContextMenu.vue"),
@@ -938,7 +970,7 @@ export function post(
 	props: InstanceType<typeof MkPostFormDialog>["$props"] = {},
 	onClosed?: () => void,
 ) {
-	return new Promise<void>((resolve, reject) => {
+	return new Promise<void>((resolve, _reject) => {
 		// NOTE: MkPostFormDialogをdynamic importするとiOSでテキストエリアに自動フォーカスできない
 		// NOTE: ただ、dynamic importしない場合、MkPostFormDialogインスタンスが使いまわされ、
 		//       Vueが渡されたコンポーネントに内部的に__propsというプロパティを生やす影響で、
@@ -966,7 +998,7 @@ export const deckGlobalEvents = new EventEmitter();
 
 /*
 export function checkExistence(fileData: ArrayBuffer): Promise<any> {
-	return new Promise((resolve, reject) => {
+	return new Promise((resolve, _reject) => {
 		const data = new FormData();
 		data.append('md5', getMD5(fileData));
 
diff --git a/packages/client/src/pages/page-editor/els/page-editor.el.image.vue b/packages/client/src/pages/page-editor/els/page-editor.el.image.vue
index 859f0f1112..3820b140df 100644
--- a/packages/client/src/pages/page-editor/els/page-editor.el.image.vue
+++ b/packages/client/src/pages/page-editor/els/page-editor.el.image.vue
@@ -44,9 +44,9 @@ const props = withDefaults(
 const file = ref<any>(null);
 
 async function choose() {
-	os.selectDriveFile(false).then((fileResponse: any) => {
+	os.selectDriveFile(false).then((fileResponse) => {
 		file.value = fileResponse;
-		props.value.fileId = fileResponse.id;
+		props.value.fileId = fileResponse?.id;
 	});
 }
 
diff --git a/packages/client/src/pages/settings/preferences-backups.vue b/packages/client/src/pages/settings/preferences-backups.vue
index 202de0a082..73e098c4ea 100644
--- a/packages/client/src/pages/settings/preferences-backups.vue
+++ b/packages/client/src/pages/settings/preferences-backups.vue
@@ -299,12 +299,12 @@ function loadFile(): void {
 		});
 
 		// 一応廃棄
-		(window as any).__misskey_input_ref__ = null;
+		window.__misskey_input_ref__ = null;
 	};
 
 	// https://qiita.com/fukasawah/items/b9dc732d95d99551013d
 	// iOS Safari で正常に動かす為のおまじない
-	(window as any).__misskey_input_ref__ = input;
+	window.__misskey_input_ref__ = input;
 
 	input.click();
 }
diff --git a/packages/client/src/pages/user-list-timeline.vue b/packages/client/src/pages/user-list-timeline.vue
index 69f17c1047..5543f768c9 100644
--- a/packages/client/src/pages/user-list-timeline.vue
+++ b/packages/client/src/pages/user-list-timeline.vue
@@ -56,8 +56,8 @@ async function timetravel() {
 		title: i18n.ts.date,
 	});
 	if (canceled) return;
-
-	tlEl.value.timetravel(date);
+	// FIXME:
+	tlEl.value!.timetravel(date);
 }
 
 const headerActions = computed(() =>
diff --git a/packages/client/src/scripts/form.ts b/packages/client/src/scripts/form.ts
index 2cd0ca01dc..dfc1c646dc 100644
--- a/packages/client/src/scripts/form.ts
+++ b/packages/client/src/scripts/form.ts
@@ -1,72 +1,11 @@
-export type FormItem =
-	| {
-			label?: string;
-			type: "string";
-			default: string | null;
-			hidden?: boolean;
-			multiline?: boolean;
-	  }
-	| {
-			label?: string;
-			type: "number";
-			default: number | null;
-			hidden?: boolean;
-			step?: number;
-	  }
-	| {
-			label?: string;
-			type: "boolean";
-			default: boolean | null;
-			hidden?: boolean;
-	  }
-	| {
-			label?: string;
-			type: "enum";
-			default: string | null;
-			hidden?: boolean;
-			enum: string[];
-	  }
-	| {
-			label?: string;
-			type: "radio";
-			default: unknown | null;
-			hidden?: boolean;
-			options: {
-				label: string;
-				value: unknown;
-			}[];
-	  }
-	| {
-			label?: string;
-			type: "object";
-			default: Record<string, unknown> | null;
-			hidden: true;
-	  }
-	| {
-			label?: string;
-			type: "array";
-			default: unknown[] | null;
-			hidden: true;
-	  };
+// TODO: replace this file with @/types/form.ts
+
+import type { FormItemType, GetFormItemResultType } from "@/types/form";
+
+export type FormItem = FormItemType;
 
 export type Form = Record<string, FormItem>;
 
-type GetItemType<Item extends FormItem> = Item["type"] extends "string"
-	? string
-	: Item["type"] extends "number"
-		? number
-		: Item["type"] extends "boolean"
-			? boolean
-			: Item["type"] extends "radio"
-				? unknown
-				: Item["type"] extends "enum"
-					? string
-					: Item["type"] extends "array"
-						? unknown[]
-						: Item["type"] extends "object"
-							? Record<string, unknown>
-							: never;
-
 export type GetFormResultType<F extends Form> = {
-	[P in keyof F]: GetItemType<F[P]>;
+	[P in keyof F]: NonNullable<GetFormItemResultType<F[P]["type"]>>;
 };
diff --git a/packages/client/src/scripts/get-note-menu.ts b/packages/client/src/scripts/get-note-menu.ts
index 873b32d7d7..25091f2da2 100644
--- a/packages/client/src/scripts/get-note-menu.ts
+++ b/packages/client/src/scripts/get-note-menu.ts
@@ -14,6 +14,7 @@ import icon from "@/scripts/icon";
 import { useRouter } from "@/router";
 import { notePage } from "@/filters/note";
 import type { NoteTranslation } from "@/types/note";
+import type { MenuItem } from "@/types/menu";
 
 const router = useRouter();
 
@@ -291,7 +292,7 @@ export function getNoteMenu(props: {
 		props.translating.value = false;
 	}
 
-	let menu;
+	let menu: MenuItem[];
 	if (isSignedIn) {
 		const statePromise = os.api("notes/state", {
 			noteId: appearNote.id,
@@ -396,7 +397,7 @@ export function getNoteMenu(props: {
 					}
 				: undefined,
 			{
-				type: "parent",
+				type: "parent" as const,
 				icon: `${icon("ph-share-network")}`,
 				text: i18n.ts.share,
 				children: [
@@ -499,7 +500,7 @@ export function getNoteMenu(props: {
 			!isAppearAuthor ? null : undefined,
 			!isAppearAuthor
 				? {
-						type: "parent",
+						type: "parent" as const,
 						icon: `${icon("ph-user")}`,
 						text: i18n.ts.user,
 						children: getUserMenu(appearNote.user),
diff --git a/packages/client/src/scripts/select-file.ts b/packages/client/src/scripts/select-file.ts
index adf9f0956a..6b51e5fef9 100644
--- a/packages/client/src/scripts/select-file.ts
+++ b/packages/client/src/scripts/select-file.ts
@@ -9,12 +9,14 @@ import icon from "@/scripts/icon";
 
 const stream = useStream();
 
-function select(
-	src: any,
+function select<Multiple extends boolean>(
+	src: HTMLElement | null | undefined,
 	label: string | null,
-	multiple: boolean,
-): Promise<entities.DriveFile | entities.DriveFile[]> {
-	return new Promise((res, rej) => {
+	multiple: Multiple,
+) {
+	return new Promise<
+		Multiple extends true ? entities.DriveFile[] : entities.DriveFile
+	>((res, rej) => {
 		const keepOriginal = ref(defaultStore.state.keepOriginalUploading);
 
 		const chooseFileFromPc = () => {
@@ -22,6 +24,9 @@ function select(
 			input.type = "file";
 			input.multiple = multiple;
 			input.onchange = () => {
+				if (input.files === null) {
+					return;
+				}
 				const promises = Array.from(input.files).map((file) =>
 					uploadFile(
 						file,
@@ -33,19 +38,19 @@ function select(
 
 				Promise.all(promises)
 					.then((driveFiles) => {
-						res(multiple ? driveFiles : driveFiles[0]);
+						res((multiple ? driveFiles : driveFiles[0]) as never);
 					})
 					.catch((err) => {
 						// アップロードのエラーは uploadFile 内でハンドリングされているためアラートダイアログを出したりはしてはいけない
 					});
 
 				// 一応廃棄
-				(window as any).__misskey_input_ref__ = null;
+				window.__misskey_input_ref__ = null;
 			};
 
 			// https://qiita.com/fukasawah/items/b9dc732d95d99551013d
 			// iOS Safari で正常に動かす為のおまじない
-			(window as any).__misskey_input_ref__ = input;
+			window.__misskey_input_ref__ = input;
 
 			input.click();
 		};
@@ -69,7 +74,7 @@ function select(
 				const connection = stream.useChannel("main");
 				connection.on("urlUploadFinished", (urlResponse) => {
 					if (urlResponse.marker === marker) {
-						res(multiple ? [urlResponse.file] : urlResponse.file);
+						res((multiple ? [urlResponse.file] : urlResponse.file) as never);
 						connection.dispose();
 					}
 				});
@@ -122,15 +127,15 @@ function select(
 }
 
 export function selectFile(
-	src: any,
+	src: HTMLElement | null | undefined,
 	label: string | null = null,
-): Promise<entities.DriveFile> {
-	return select(src, label, false) as Promise<entities.DriveFile>;
+) {
+	return select(src, label, false);
 }
 
 export function selectFiles(
-	src: any,
+	src: HTMLElement | null | undefined,
 	label: string | null = null,
-): Promise<entities.DriveFile[]> {
-	return select(src, label, true) as Promise<entities.DriveFile[]>;
+) {
+	return select(src, label, true);
 }
diff --git a/packages/client/src/types/form.ts b/packages/client/src/types/form.ts
index 9c34df30dc..88f90ebbef 100644
--- a/packages/client/src/types/form.ts
+++ b/packages/client/src/types/form.ts
@@ -54,18 +54,18 @@ export type FormItemSwitch = BaseFormItem & {
 };
 export type FormItemSelect = BaseFormItem & {
 	type: "enum";
-	default?: string | null;
+	default?: string | number | symbol | null;
 	enum: {
-		value: string | number | symbol | undefined;
+		value: string | number | symbol;
 		label: string;
 	}[];
 };
 export type FormItemRadios = BaseFormItem & {
 	type: "radio";
-	default?: string | number | symbol | undefined | null;
+	default?: string | number | symbol | null;
 	options: {
 		label: string;
-		value: string | number | symbol | undefined;
+		value: string | number | symbol;
 	}[];
 };
 export type FormItemRange = BaseFormItem & {
@@ -114,10 +114,25 @@ export type FormItemInput = FormItemInputArray[number];
 
 export type FormItemType = FormItemTypeArray[number];
 
-export type GetFormItemByType<T extends FormItemType["type"], F extends FormItemType = FormItemType> = F extends {type: T} ? F : never;
+export type Form = Record<string, FormItemType>;
+
+export type GetFormItemByType<
+	T extends FormItemType["type"],
+	F extends FormItemType = FormItemType,
+> = F extends { type: T } ? F : never;
 
 type NonUndefindAble<T> = T extends undefined ? never : T;
+type NonNullAble<T> = T extends null ? never : T;
 
-export type GetFormResultType<T extends FormItemType["type"], I extends FormItemType = GetFormItemByType<T>> = NonUndefindAble<
+export type GetFormItemResultType<
+	T extends FormItemType["type"],
+	I extends FormItemType = GetFormItemByType<T>,
+> = NonUndefindAble<
 	"__result_typedef" extends keyof I ? I["__result_typedef"] : I["default"]
->
+>;
+
+export type GetFormResultType<F extends Form> = {
+	[K in keyof F]: F[K]["required"] extends false
+		? GetFormItemResultType<F[K]["type"]>
+		: NonNullAble<GetFormItemResultType<F[K]["type"]>>;
+};
diff --git a/packages/client/src/widgets/widget.ts b/packages/client/src/widgets/widget.ts
index 9f33cecfa2..1fdcca86f1 100644
--- a/packages/client/src/widgets/widget.ts
+++ b/packages/client/src/widgets/widget.ts
@@ -23,9 +23,7 @@ export interface WidgetComponentExpose {
 	configure: () => void;
 }
 
-export const useWidgetPropsManager = <
-	F extends Form & Record<string, { default: any }>,
->(
+export const useWidgetPropsManager = <F extends Form>(
 	name: string,
 	propsDef: F,
 	props: Readonly<WidgetComponentProps<GetFormResultType<F>>>,
diff --git a/packages/firefish-js/src/api.ts b/packages/firefish-js/src/api.ts
index 2639712a3f..4a7e1f0b64 100644
--- a/packages/firefish-js/src/api.ts
+++ b/packages/firefish-js/src/api.ts
@@ -122,6 +122,6 @@ export class APIClient {
 				.catch(reject);
 		});
 
-		return promise as any;
+		return promise;
 	}
 }
diff --git a/packages/firefish-js/src/api.types.ts b/packages/firefish-js/src/api.types.ts
index 30ae1f9cdf..c0a7c9019b 100644
--- a/packages/firefish-js/src/api.types.ts
+++ b/packages/firefish-js/src/api.types.ts
@@ -36,6 +36,7 @@ import type {
 	UserDetailed,
 	UserGroup,
 	UserList,
+	UserLite,
 	UserSorting,
 } from "./entities";
 

From 8a62bf90f59d18a268832c87e61c0994ee1f2c6e Mon Sep 17 00:00:00 2001
From: Lhcfl <Lhcfl@outlook.com>
Date: Sun, 14 Apr 2024 03:42:30 +0800
Subject: [PATCH 052/110] fix: MkMenu.vue

---
 packages/client/src/components/MkMenu.vue | 9 +++++++--
 1 file changed, 7 insertions(+), 2 deletions(-)

diff --git a/packages/client/src/components/MkMenu.vue b/packages/client/src/components/MkMenu.vue
index e92305d8f3..1391024264 100644
--- a/packages/client/src/components/MkMenu.vue
+++ b/packages/client/src/components/MkMenu.vue
@@ -202,12 +202,12 @@
 
 <script lang="ts" setup>
 import {
+	type Ref,
 	defineAsyncComponent,
 	onBeforeUnmount,
 	onMounted,
 	ref,
 	watch,
-	shallowRef,
 } from "vue";
 import { FocusTrap } from "focus-trap-vue";
 import FormSwitch from "@/components/form/switch.vue";
@@ -242,7 +242,12 @@ const emit = defineEmits<{
 
 const itemsEl = ref<HTMLDivElement>();
 
-const items2 = shallowRef<InnerMenuItem[]>([]);
+/**
+ * Strictly speaking, this type conversion is wrong
+ * because `ref` will deeply unpack the `ref` in `MenuSwitch`.  
+ * But it performs correctly, so who cares?
+ */
+const items2 = ref([]) as Ref<InnerMenuItem[]>;
 
 const child = ref<InstanceType<typeof XChild>>();
 

From 19b45866c804ab0667b26acfa7a84c5c1151874c Mon Sep 17 00:00:00 2001
From: naskya <m@naskya.net>
Date: Sun, 14 Apr 2024 13:35:35 +0900
Subject: [PATCH 053/110] docs: update packages/README.md

---
 packages/README.md | 1 +
 1 file changed, 1 insertion(+)

diff --git a/packages/README.md b/packages/README.md
index 56356ec2d0..6637462910 100644
--- a/packages/README.md
+++ b/packages/README.md
@@ -4,6 +4,7 @@ This directory contains all of the packages Firefish uses.
 
 - `backend`: Main backend code written in TypeScript for NodeJS
 - `backend-rs`: Backend code written in Rust, bound to NodeJS by [NAPI-RS](https://napi.rs/)
+- `macro-rs`: Procedural macros for backend-rs
 - `client`: Web interface written in Vue3 and TypeScript
 - `sw`: Web [Service Worker](https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API) written in TypeScript
 - `firefish-js`: TypeScript SDK for both backend and client

From baa57d7c173433f4b30f949fd1246960afebb7a0 Mon Sep 17 00:00:00 2001
From: naskya <m@naskya.net>
Date: Sun, 14 Apr 2024 13:38:44 +0900
Subject: [PATCH 054/110] dev: add rust-analyzer to recommended VSCode
 extensitons

---
 .vscode/extensions.json | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/.vscode/extensions.json b/.vscode/extensions.json
index 0083604d44..de0385be2a 100644
--- a/.vscode/extensions.json
+++ b/.vscode/extensions.json
@@ -12,6 +12,7 @@
     "esbenp.prettier-vscode",
     "redhat.vscode-yaml",
     "yoavbls.pretty-ts-errors",
-    "biomejs.biome"
+    "biomejs.biome",
+		"rust-lang.rust-analyzer"
   ]
 }

From 70aa3704ef27932ef0d3da543f169c0099f103bb Mon Sep 17 00:00:00 2001
From: sup39 <dev@sup39.dev>
Date: Sun, 14 Apr 2024 14:41:01 +0900
Subject: [PATCH 055/110] refactor (backend): port password
 hashing/verification to backend-rs

Co-authored-by: naskya <m@naskya.net>
---
 Cargo.lock                                    | 92 ++++++++++++++++++-
 Cargo.toml                                    |  2 +
 packages/backend-rs/Cargo.toml                |  2 +
 packages/backend-rs/index.d.ts                |  3 +
 packages/backend-rs/index.js                  |  5 +-
 packages/backend-rs/src/misc/mod.rs           |  1 +
 packages/backend-rs/src/misc/password.rs      | 69 ++++++++++++++
 packages/backend/package.json                 |  3 -
 packages/backend/src/misc/password.ts         | 20 ----
 .../backend/src/server/api/common/signup.ts   |  5 +-
 .../api/endpoints/admin/reset-password.ts     |  6 +-
 .../server/api/endpoints/i/2fa/key-done.ts    |  6 +-
 .../api/endpoints/i/2fa/register-key.ts       |  7 +-
 .../server/api/endpoints/i/2fa/register.ts    |  6 +-
 .../server/api/endpoints/i/2fa/remove-key.ts  |  6 +-
 .../server/api/endpoints/i/2fa/unregister.ts  |  6 +-
 .../server/api/endpoints/i/change-password.ts |  6 +-
 .../server/api/endpoints/i/delete-account.ts  |  6 +-
 .../api/endpoints/i/regenerate-token.ts       |  6 +-
 .../server/api/endpoints/i/update-email.ts    |  6 +-
 .../server/api/endpoints/reset-password.ts    |  4 +-
 .../backend/src/server/api/private/signin.ts  | 16 ++--
 .../backend/src/server/api/private/signup.ts  |  6 +-
 .../src/services/create-system-user.ts        |  5 +-
 pnpm-lock.yaml                                | 38 +-------
 25 files changed, 214 insertions(+), 118 deletions(-)
 create mode 100644 packages/backend-rs/src/misc/password.rs
 delete mode 100644 packages/backend/src/misc/password.ts

diff --git a/Cargo.lock b/Cargo.lock
index 93c475fa0b..6552351a3a 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -132,6 +132,18 @@ version = "1.0.82"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "f538837af36e6f6a9be0faa67f9a314f8119e4e4b5867c6ab40ed60360142519"
 
+[[package]]
+name = "argon2"
+version = "0.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072"
+dependencies = [
+ "base64ct",
+ "blake2",
+ "cpufeatures",
+ "password-hash",
+]
+
 [[package]]
 name = "arrayvec"
 version = "0.7.4"
@@ -190,8 +202,10 @@ checksum = "f1fdabc7756949593fe60f30ec81974b613357de856987752631dea1e3394c80"
 name = "backend-rs"
 version = "0.0.0"
 dependencies = [
+ "argon2",
  "async-trait",
  "basen",
+ "bcrypt",
  "cfg-if",
  "chrono",
  "cuid2",
@@ -238,6 +252,12 @@ version = "0.21.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567"
 
+[[package]]
+name = "base64"
+version = "0.22.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9475866fec1451be56a3c2400fd081ff546538961565ccb5b7142cbd22bc7a51"
+
 [[package]]
 name = "base64ct"
 version = "1.6.0"
@@ -250,6 +270,19 @@ version = "0.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "1dbe4bb73fd931c4d1aaf53b35d1286c8a948ad00ec92c8e3c856f15fd027f43"
 
+[[package]]
+name = "bcrypt"
+version = "0.15.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e65938ed058ef47d92cf8b346cc76ef48984572ade631927e9937b5ffc7662c7"
+dependencies = [
+ "base64 0.22.0",
+ "blowfish",
+ "getrandom",
+ "subtle",
+ "zeroize",
+]
+
 [[package]]
 name = "bigdecimal"
 version = "0.3.1"
@@ -303,6 +336,15 @@ dependencies = [
  "wyz",
 ]
 
+[[package]]
+name = "blake2"
+version = "0.10.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe"
+dependencies = [
+ "digest",
+]
+
 [[package]]
 name = "block-buffer"
 version = "0.10.4"
@@ -312,6 +354,16 @@ dependencies = [
  "generic-array",
 ]
 
+[[package]]
+name = "blowfish"
+version = "0.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e412e2cd0f2b2d93e02543ceae7917b3c70331573df19ee046bcbc35e45e87d7"
+dependencies = [
+ "byteorder",
+ "cipher",
+]
+
 [[package]]
 name = "borsh"
 version = "1.4.0"
@@ -415,6 +467,16 @@ dependencies = [
  "windows-targets 0.52.4",
 ]
 
+[[package]]
+name = "cipher"
+version = "0.4.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad"
+dependencies = [
+ "crypto-common",
+ "inout",
+]
+
 [[package]]
 name = "clap"
 version = "4.5.4"
@@ -1075,6 +1137,15 @@ dependencies = [
  "syn 2.0.58",
 ]
 
+[[package]]
+name = "inout"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5"
+dependencies = [
+ "generic-array",
+]
+
 [[package]]
 name = "ipnet"
 version = "2.9.0"
@@ -1122,7 +1193,7 @@ checksum = "2a071f4f7efc9a9118dfb627a0a94ef247986e1ab8606a4c806ae2b3aa3b6978"
 dependencies = [
  "ahash 0.8.11",
  "anyhow",
- "base64",
+ "base64 0.21.7",
  "bytecount",
  "clap",
  "fancy-regex",
@@ -1559,6 +1630,17 @@ dependencies = [
  "syn 2.0.58",
 ]
 
+[[package]]
+name = "password-hash"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166"
+dependencies = [
+ "base64ct",
+ "rand_core",
+ "subtle",
+]
+
 [[package]]
 name = "paste"
 version = "1.0.14"
@@ -1801,7 +1883,7 @@ version = "0.11.27"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "dd67538700a17451e7cba03ac727fb961abb7607553461627b97de0b89cf4a62"
 dependencies = [
- "base64",
+ "base64 0.21.7",
  "bytes",
  "encoding_rs",
  "futures-core",
@@ -1947,7 +2029,7 @@ version = "1.0.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c"
 dependencies = [
- "base64",
+ "base64 0.21.7",
 ]
 
 [[package]]
@@ -2398,7 +2480,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "1ed31390216d20e538e447a7a9b959e06ed9fc51c37b514b46eb758016ecd418"
 dependencies = [
  "atoi",
- "base64",
+ "base64 0.21.7",
  "bigdecimal",
  "bitflags 2.5.0",
  "byteorder",
@@ -2445,7 +2527,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "7c824eb80b894f926f89a0b9da0c7f435d27cdd35b8c655b114e58223918577e"
 dependencies = [
  "atoi",
- "base64",
+ "base64 0.21.7",
  "bigdecimal",
  "bitflags 2.5.0",
  "byteorder",
diff --git a/Cargo.toml b/Cargo.toml
index f7ba12d884..1a72cb2c5a 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -9,8 +9,10 @@ napi = { version = "2.16.2", default-features = false }
 napi-derive = "2.16.2"
 napi-build = "2.1.2"
 
+argon2 = "0.5.3"
 async-trait = "0.1.80"
 basen = "0.1.0"
+bcrypt = "0.15.1"
 cfg-if = "1.0.0"
 chrono = "0.4.37"
 convert_case = "0.6.0"
diff --git a/packages/backend-rs/Cargo.toml b/packages/backend-rs/Cargo.toml
index 783f630942..235fcc8706 100644
--- a/packages/backend-rs/Cargo.toml
+++ b/packages/backend-rs/Cargo.toml
@@ -17,8 +17,10 @@ macro_rs = { workspace = true }
 napi = { workspace = true, optional = true, default-features = false, features = ["napi9", "tokio_rt", "chrono_date", "serde-json"] }
 napi-derive = { workspace = true, optional = true }
 
+argon2 = { workspace = true, features = ["std"] }
 async-trait = { workspace = true }
 basen = { workspace = true }
+bcrypt = { workspace = true }
 cfg-if = { workspace = true }
 chrono = { workspace = true }
 cuid2 = { workspace = true }
diff --git a/packages/backend-rs/index.d.ts b/packages/backend-rs/index.d.ts
index 617bedf5b9..f2830dcac3 100644
--- a/packages/backend-rs/index.d.ts
+++ b/packages/backend-rs/index.d.ts
@@ -135,6 +135,9 @@ export function toPuny(host: string): string
 export function toMastodonId(firefishId: string): string | null
 export function fromMastodonId(mastodonId: string): string | null
 export function nyaify(text: string, lang?: string | undefined | null): string
+export function hashPassword(password: string): string
+export function verifyPassword(password: string, hash: string): boolean
+export function isOldPasswordAlgorithm(hash: string): boolean
 export interface AbuseUserReport {
   id: string
   createdAt: Date
diff --git a/packages/backend-rs/index.js b/packages/backend-rs/index.js
index f2942a9aa5..af9a293bbb 100644
--- a/packages/backend-rs/index.js
+++ b/packages/backend-rs/index.js
@@ -310,7 +310,7 @@ if (!nativeBinding) {
   throw new Error(`Failed to load native binding`)
 }
 
-const { readServerConfig, stringToAcct, acctToString, checkWordMute, getFullApAccount, isSelfHost, isSameOrigin, extractHost, toPuny, toMastodonId, fromMastodonId, nyaify, AntennaSrcEnum, MutedNoteReasonEnum, NoteVisibilityEnum, NotificationTypeEnum, PageVisibilityEnum, PollNotevisibilityEnum, RelayStatusEnum, UserEmojimodpermEnum, UserProfileFfvisibilityEnum, UserProfileMutingnotificationtypesEnum, initIdGenerator, getTimestamp, genId, secureRndstr } = nativeBinding
+const { readServerConfig, stringToAcct, acctToString, checkWordMute, getFullApAccount, isSelfHost, isSameOrigin, extractHost, toPuny, toMastodonId, fromMastodonId, nyaify, hashPassword, verifyPassword, isOldPasswordAlgorithm, AntennaSrcEnum, MutedNoteReasonEnum, NoteVisibilityEnum, NotificationTypeEnum, PageVisibilityEnum, PollNotevisibilityEnum, RelayStatusEnum, UserEmojimodpermEnum, UserProfileFfvisibilityEnum, UserProfileMutingnotificationtypesEnum, initIdGenerator, getTimestamp, genId, secureRndstr } = nativeBinding
 
 module.exports.readServerConfig = readServerConfig
 module.exports.stringToAcct = stringToAcct
@@ -324,6 +324,9 @@ module.exports.toPuny = toPuny
 module.exports.toMastodonId = toMastodonId
 module.exports.fromMastodonId = fromMastodonId
 module.exports.nyaify = nyaify
+module.exports.hashPassword = hashPassword
+module.exports.verifyPassword = verifyPassword
+module.exports.isOldPasswordAlgorithm = isOldPasswordAlgorithm
 module.exports.AntennaSrcEnum = AntennaSrcEnum
 module.exports.MutedNoteReasonEnum = MutedNoteReasonEnum
 module.exports.NoteVisibilityEnum = NoteVisibilityEnum
diff --git a/packages/backend-rs/src/misc/mod.rs b/packages/backend-rs/src/misc/mod.rs
index 701e35a4eb..6eda4e449e 100644
--- a/packages/backend-rs/src/misc/mod.rs
+++ b/packages/backend-rs/src/misc/mod.rs
@@ -3,3 +3,4 @@ pub mod check_word_mute;
 pub mod convert_host;
 pub mod mastodon_id;
 pub mod nyaify;
+pub mod password;
diff --git a/packages/backend-rs/src/misc/password.rs b/packages/backend-rs/src/misc/password.rs
new file mode 100644
index 0000000000..b21ff73499
--- /dev/null
+++ b/packages/backend-rs/src/misc/password.rs
@@ -0,0 +1,69 @@
+use argon2::{
+    password_hash,
+    password_hash::{rand_core::OsRng, PasswordHash, PasswordHasher, PasswordVerifier, SaltString},
+    Argon2,
+};
+
+#[crate::export]
+pub fn hash_password(password: &str) -> Result<String, password_hash::errors::Error> {
+    let salt = SaltString::generate(&mut OsRng);
+    Ok(Argon2::default()
+        .hash_password(password.as_bytes(), &salt)?
+        .to_string())
+}
+
+#[derive(thiserror::Error, Debug)]
+pub enum VerifyError {
+    #[error("An error occured while bcrypt verification: {0}")]
+    BcryptError(#[from] bcrypt::BcryptError),
+    #[error("Invalid argon2 password hash: {0}")]
+    InvalidArgon2Hash(#[from] password_hash::Error),
+    #[error("An error occured while argon2 verification: {0}")]
+    Argon2Error(#[from] argon2::Error),
+}
+
+#[crate::export]
+pub fn verify_password(password: &str, hash: &str) -> Result<bool, VerifyError> {
+    if is_old_password_algorithm(hash) {
+        Ok(bcrypt::verify(password, hash)?)
+    } else {
+        let parsed_hash = PasswordHash::new(hash)?;
+        Ok(Argon2::default()
+            .verify_password(password.as_bytes(), &parsed_hash)
+            .is_ok())
+    }
+}
+
+#[inline]
+#[crate::export]
+pub fn is_old_password_algorithm(hash: &str) -> bool {
+    // bcrypt hashes start with $2[ab]$
+    hash.starts_with("$2")
+}
+
+#[cfg(test)]
+mod unit_test {
+    use super::{hash_password, is_old_password_algorithm, verify_password};
+
+    #[test]
+    fn verify_password_test() {
+        let password = "omWc*%sD^fn7o2cXmc9e2QasBdrbRuhNB*gx!J5";
+
+        let hash = hash_password(password).unwrap();
+        assert!(verify_password(password, hash.as_str()).unwrap());
+
+        let argon2_hash = "$argon2id$v=19$m=19456,t=2,p=1$jty3puDFd4ENv/lgHn3ROQ$kRHDdEoVv2rruvnF731E74NxnYlvj5FMgePdGIIq3Jk";
+        let argon2_invalid_hash = "$argon2id$v=19$m=19456,t=2,p=1$jty3puDFd4ENv/lgHn3ROQ$kRHDdEoVv2rruvnF731E74NxnYlvj4FMgePdGIIq3Jk";
+        let bcrypt_hash = "$2a$12$WzUc.20jgbHmQjUMqTr8vOhKqYbS1BUvubapv/GLjCK1IN.h4e4la";
+        let bcrypt_invalid_hash = "$2a$12$WzUc.20jgbHmQjUMqTr7vOhKqYbS1BUvubapv/GLjCK1IN.h4e4la";
+
+        assert!(!is_old_password_algorithm(argon2_hash));
+        assert!(is_old_password_algorithm(bcrypt_hash));
+
+        assert!(verify_password(password, argon2_hash).unwrap());
+        assert!(verify_password(password, bcrypt_hash).unwrap());
+
+        assert!(!verify_password(password, argon2_invalid_hash).unwrap());
+        assert!(!verify_password(password, bcrypt_invalid_hash).unwrap());
+    }
+}
diff --git a/packages/backend/package.json b/packages/backend/package.json
index 5382f1d361..5dbf7cad64 100644
--- a/packages/backend/package.json
+++ b/packages/backend/package.json
@@ -37,11 +37,9 @@
 		"adm-zip": "0.5.10",
 		"ajv": "8.12.0",
 		"archiver": "7.0.1",
-		"argon2": "^0.40.1",
 		"aws-sdk": "2.1597.0",
 		"axios": "^1.6.8",
 		"backend-rs": "workspace:*",
-		"bcryptjs": "2.4.3",
 		"blurhash": "2.0.5",
 		"bull": "4.12.2",
 		"cacheable-lookup": "TheEssem/cacheable-lookup",
@@ -130,7 +128,6 @@
 		"@swc/cli": "0.3.12",
 		"@swc/core": "1.4.13",
 		"@types/adm-zip": "^0.5.5",
-		"@types/bcryptjs": "2.4.6",
 		"@types/color-convert": "^2.0.3",
 		"@types/content-disposition": "^0.5.8",
 		"@types/escape-regexp": "0.0.3",
diff --git a/packages/backend/src/misc/password.ts b/packages/backend/src/misc/password.ts
deleted file mode 100644
index c63f89f5c9..0000000000
--- a/packages/backend/src/misc/password.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-import bcrypt from "bcryptjs";
-import * as argon2 from "argon2";
-
-export async function hashPassword(password: string): Promise<string> {
-	return argon2.hash(password);
-}
-
-export async function comparePassword(
-	password: string,
-	hash: string,
-): Promise<boolean> {
-	if (isOldAlgorithm(hash)) return bcrypt.compare(password, hash);
-
-	return argon2.verify(hash, password);
-}
-
-export function isOldAlgorithm(hash: string): boolean {
-	// bcrypt hashes start with $2[ab]$
-	return hash.startsWith("$2");
-}
diff --git a/packages/backend/src/server/api/common/signup.ts b/packages/backend/src/server/api/common/signup.ts
index a267baf9c0..58b88b7d02 100644
--- a/packages/backend/src/server/api/common/signup.ts
+++ b/packages/backend/src/server/api/common/signup.ts
@@ -4,12 +4,11 @@ import { User } from "@/models/entities/user.js";
 import { Users, UsedUsernames } from "@/models/index.js";
 import { UserProfile } from "@/models/entities/user-profile.js";
 import { IsNull } from "typeorm";
-import { genId, toPuny } from "backend-rs";
+import { genId, hashPassword, toPuny } from "backend-rs";
 import { UserKeypair } from "@/models/entities/user-keypair.js";
 import { UsedUsername } from "@/models/entities/used-username.js";
 import { db } from "@/db/postgre.js";
 import config from "@/config/index.js";
-import { hashPassword } from "@/misc/password.js";
 
 export async function signup(opts: {
 	username: User["username"];
@@ -40,7 +39,7 @@ export async function signup(opts: {
 		}
 
 		// Generate hash of password
-		hash = await hashPassword(password);
+		hash = hashPassword(password);
 	}
 
 	// Generate secret
diff --git a/packages/backend/src/server/api/endpoints/admin/reset-password.ts b/packages/backend/src/server/api/endpoints/admin/reset-password.ts
index 5fbed130e6..ace0b581d7 100644
--- a/packages/backend/src/server/api/endpoints/admin/reset-password.ts
+++ b/packages/backend/src/server/api/endpoints/admin/reset-password.ts
@@ -1,8 +1,7 @@
 import define from "@/server/api/define.js";
-// import bcrypt from "bcryptjs";
 import rndstr from "rndstr";
 import { Users, UserProfiles } from "@/models/index.js";
-import { hashPassword } from "@/misc/password.js";
+import { hashPassword } from "backend-rs";
 
 export const meta = {
 	tags: ["admin"],
@@ -48,8 +47,7 @@ export default define(meta, paramDef, async (ps) => {
 	const passwd = rndstr("a-zA-Z0-9", 8);
 
 	// Generate hash of password
-	// const hash = bcrypt.hashSync(passwd);
-	const hash = await hashPassword(passwd);
+	const hash = hashPassword(passwd);
 
 	await UserProfiles.update(
 		{
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 0e52dd0d78..6c99217e7d 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
@@ -9,7 +9,7 @@ import {
 import config from "@/config/index.js";
 import { procedures, hash } from "@/server/api/2fa.js";
 import { publishMainStream } from "@/services/stream.js";
-import { comparePassword } from "@/misc/password.js";
+import { verifyPassword } from "backend-rs";
 
 const rpIdHashReal = hash(Buffer.from(config.hostname, "utf-8"));
 
@@ -40,8 +40,8 @@ export const paramDef = {
 export default define(meta, paramDef, async (ps, user) => {
 	const profile = await UserProfiles.findOneByOrFail({ userId: user.id });
 
-	// Compare password
-	const same = await comparePassword(ps.password, profile.password!);
+	// Compare passwords
+	const same = verifyPassword(ps.password, profile.password!);
 
 	if (!same) {
 		throw new Error("incorrect password");
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 b275b5705d..4991e8fc90 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
@@ -2,9 +2,8 @@ import define from "@/server/api/define.js";
 import { UserProfiles, AttestationChallenges } from "@/models/index.js";
 import { promisify } from "node:util";
 import * as crypto from "node:crypto";
-import { genId } from "backend-rs";
+import { genId, verifyPassword } from "backend-rs";
 import { hash } from "@/server/api/2fa.js";
-import { comparePassword } from "@/misc/password.js";
 
 const randomBytes = promisify(crypto.randomBytes);
 
@@ -25,8 +24,8 @@ export const paramDef = {
 export default define(meta, paramDef, async (ps, user) => {
 	const profile = await UserProfiles.findOneByOrFail({ userId: user.id });
 
-	// Compare password
-	const same = await comparePassword(ps.password, profile.password!);
+	// Compare passwords
+	const same = verifyPassword(ps.password, profile.password!);
 
 	if (!same) {
 		throw new Error("incorrect password");
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 52e1df39f4..c0e6137d5d 100644
--- a/packages/backend/src/server/api/endpoints/i/2fa/register.ts
+++ b/packages/backend/src/server/api/endpoints/i/2fa/register.ts
@@ -3,7 +3,7 @@ import * as QRCode from "qrcode";
 import config from "@/config/index.js";
 import { UserProfiles } from "@/models/index.js";
 import define from "@/server/api/define.js";
-import { comparePassword } from "@/misc/password.js";
+import { verifyPassword } from "backend-rs";
 
 export const meta = {
 	requireCredential: true,
@@ -22,8 +22,8 @@ export const paramDef = {
 export default define(meta, paramDef, async (ps, user) => {
 	const profile = await UserProfiles.findOneByOrFail({ userId: user.id });
 
-	// Compare password
-	const same = await comparePassword(ps.password, profile.password!);
+	// Compare passwords
+	const same = verifyPassword(ps.password, profile.password!);
 
 	if (!same) {
 		throw new Error("incorrect password");
diff --git a/packages/backend/src/server/api/endpoints/i/2fa/remove-key.ts b/packages/backend/src/server/api/endpoints/i/2fa/remove-key.ts
index 0cdf8780ef..4259d8f70d 100644
--- a/packages/backend/src/server/api/endpoints/i/2fa/remove-key.ts
+++ b/packages/backend/src/server/api/endpoints/i/2fa/remove-key.ts
@@ -1,4 +1,4 @@
-import { comparePassword } from "@/misc/password.js";
+import { verifyPassword } from "backend-rs";
 import define from "@/server/api/define.js";
 import { UserProfiles, UserSecurityKeys, Users } from "@/models/index.js";
 import { publishMainStream } from "@/services/stream.js";
@@ -21,8 +21,8 @@ export const paramDef = {
 export default define(meta, paramDef, async (ps, user) => {
 	const profile = await UserProfiles.findOneByOrFail({ userId: user.id });
 
-	// Compare password
-	const same = await comparePassword(ps.password, profile.password!);
+	// Compare passwords
+	const same = verifyPassword(ps.password, profile.password!);
 
 	if (!same) {
 		throw new Error("incorrect password");
diff --git a/packages/backend/src/server/api/endpoints/i/2fa/unregister.ts b/packages/backend/src/server/api/endpoints/i/2fa/unregister.ts
index c4e78eecb5..240ff2b34e 100644
--- a/packages/backend/src/server/api/endpoints/i/2fa/unregister.ts
+++ b/packages/backend/src/server/api/endpoints/i/2fa/unregister.ts
@@ -1,7 +1,7 @@
 import { publishMainStream } from "@/services/stream.js";
 import define from "@/server/api/define.js";
 import { Users, UserProfiles } from "@/models/index.js";
-import { comparePassword } from "@/misc/password.js";
+import { verifyPassword } from "backend-rs";
 
 export const meta = {
 	requireCredential: true,
@@ -20,8 +20,8 @@ export const paramDef = {
 export default define(meta, paramDef, async (ps, user) => {
 	const profile = await UserProfiles.findOneByOrFail({ userId: user.id });
 
-	// Compare password
-	const same = await comparePassword(ps.password, profile.password!);
+	// Compare passwords
+	const same = verifyPassword(ps.password, profile.password!);
 
 	if (!same) {
 		throw new Error("incorrect password");
diff --git a/packages/backend/src/server/api/endpoints/i/change-password.ts b/packages/backend/src/server/api/endpoints/i/change-password.ts
index b0dc8bba60..1634676748 100644
--- a/packages/backend/src/server/api/endpoints/i/change-password.ts
+++ b/packages/backend/src/server/api/endpoints/i/change-password.ts
@@ -1,6 +1,6 @@
 import define from "@/server/api/define.js";
 import { UserProfiles } from "@/models/index.js";
-import { hashPassword, comparePassword } from "@/misc/password.js";
+import { hashPassword, verifyPassword } from "backend-rs";
 
 export const meta = {
 	requireCredential: true,
@@ -20,8 +20,8 @@ export const paramDef = {
 export default define(meta, paramDef, async (ps, user) => {
 	const profile = await UserProfiles.findOneByOrFail({ userId: user.id });
 
-	// Compare password
-	const same = await comparePassword(ps.currentPassword, profile.password!);
+	// Compare passwords
+	const same = verifyPassword(ps.currentPassword, profile.password!);
 
 	if (!same) {
 		throw new Error("incorrect password");
diff --git a/packages/backend/src/server/api/endpoints/i/delete-account.ts b/packages/backend/src/server/api/endpoints/i/delete-account.ts
index 606cde82e1..538798261d 100644
--- a/packages/backend/src/server/api/endpoints/i/delete-account.ts
+++ b/packages/backend/src/server/api/endpoints/i/delete-account.ts
@@ -1,7 +1,7 @@
 import { UserProfiles, Users } from "@/models/index.js";
 import { deleteAccount } from "@/services/delete-account.js";
 import define from "@/server/api/define.js";
-import { comparePassword } from "@/misc/password.js";
+import { verifyPassword } from "backend-rs";
 
 export const meta = {
 	requireCredential: true,
@@ -24,8 +24,8 @@ export default define(meta, paramDef, async (ps, user) => {
 		return;
 	}
 
-	// Compare password
-	const same = await comparePassword(ps.password, profile.password!);
+	// Compare passwords
+	const same = verifyPassword(ps.password, profile.password!);
 
 	if (!same) {
 		throw new Error("incorrect password");
diff --git a/packages/backend/src/server/api/endpoints/i/regenerate-token.ts b/packages/backend/src/server/api/endpoints/i/regenerate-token.ts
index c1b4325adb..fd3023ab7a 100644
--- a/packages/backend/src/server/api/endpoints/i/regenerate-token.ts
+++ b/packages/backend/src/server/api/endpoints/i/regenerate-token.ts
@@ -6,7 +6,7 @@ import {
 import generateUserToken from "@/server/api/common/generate-native-user-token.js";
 import define from "@/server/api/define.js";
 import { Users, UserProfiles } from "@/models/index.js";
-import { comparePassword } from "@/misc/password.js";
+import { verifyPassword } from "backend-rs";
 
 export const meta = {
 	requireCredential: true,
@@ -28,8 +28,8 @@ export default define(meta, paramDef, async (ps, user) => {
 
 	const profile = await UserProfiles.findOneByOrFail({ userId: user.id });
 
-	// Compare password
-	const same = await comparePassword(ps.password, profile.password!);
+	// Compare passwords
+	const same = verifyPassword(ps.password, profile.password!);
 
 	if (!same) {
 		throw new Error("incorrect password");
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 a48252ed1a..234127f584 100644
--- a/packages/backend/src/server/api/endpoints/i/update-email.ts
+++ b/packages/backend/src/server/api/endpoints/i/update-email.ts
@@ -7,7 +7,7 @@ import { sendEmail } from "@/services/send-email.js";
 import { ApiError } from "@/server/api/error.js";
 import { validateEmailForAccount } from "@/services/validate-email-for-account.js";
 import { HOUR } from "@/const.js";
-import { comparePassword } from "@/misc/password.js";
+import { verifyPassword } from "backend-rs";
 
 export const meta = {
 	requireCredential: true,
@@ -46,8 +46,8 @@ export const paramDef = {
 export default define(meta, paramDef, async (ps, user) => {
 	const profile = await UserProfiles.findOneByOrFail({ userId: user.id });
 
-	// Compare password
-	const same = await comparePassword(ps.password, profile.password!);
+	// Compare passwords
+	const same = verifyPassword(ps.password, profile.password!);
 
 	if (!same) {
 		throw new ApiError(meta.errors.incorrectPassword);
diff --git a/packages/backend/src/server/api/endpoints/reset-password.ts b/packages/backend/src/server/api/endpoints/reset-password.ts
index ff5c8d987f..b69b1b17d3 100644
--- a/packages/backend/src/server/api/endpoints/reset-password.ts
+++ b/packages/backend/src/server/api/endpoints/reset-password.ts
@@ -1,6 +1,6 @@
 import { UserProfiles, PasswordResetRequests } from "@/models/index.js";
 import define from "@/server/api/define.js";
-import { hashPassword } from "@/misc/password.js";
+import { hashPassword } from "backend-rs";
 
 export const meta = {
 	tags: ["reset password"],
@@ -32,7 +32,7 @@ export default define(meta, paramDef, async (ps, user) => {
 	}
 
 	// Generate hash of password
-	const hash = await hashPassword(ps.password);
+	const hash = hashPassword(ps.password);
 
 	await UserProfiles.update(req.userId, {
 		password: hash,
diff --git a/packages/backend/src/server/api/private/signin.ts b/packages/backend/src/server/api/private/signin.ts
index 8692b01ad1..a7eb623062 100644
--- a/packages/backend/src/server/api/private/signin.ts
+++ b/packages/backend/src/server/api/private/signin.ts
@@ -10,12 +10,12 @@ import {
 	AttestationChallenges,
 } from "@/models/index.js";
 import type { ILocalUser } from "@/models/entities/user.js";
-import { genId } from "backend-rs";
 import {
-	comparePassword,
+	genId,
 	hashPassword,
-	isOldAlgorithm,
-} from "@/misc/password.js";
+	isOldPasswordAlgorithm,
+	verifyPassword,
+} from "backend-rs";
 import { verifyLogin, hash } from "@/server/api/2fa.js";
 import { randomBytes } from "node:crypto";
 import { IsNull } from "typeorm";
@@ -91,11 +91,11 @@ export default async (ctx: Koa.Context) => {
 
 	const profile = await UserProfiles.findOneByOrFail({ userId: user.id });
 
-	// Compare password
-	const same = await comparePassword(password, profile.password!);
+	// Compare passwords
+	const same = verifyPassword(password, profile.password!);
 
-	if (same && isOldAlgorithm(profile.password!)) {
-		profile.password = await hashPassword(password);
+	if (same && isOldPasswordAlgorithm(profile.password!)) {
+		profile.password = hashPassword(password);
 		await UserProfiles.save(profile);
 	}
 
diff --git a/packages/backend/src/server/api/private/signup.ts b/packages/backend/src/server/api/private/signup.ts
index 8179300884..64b5c57337 100644
--- a/packages/backend/src/server/api/private/signup.ts
+++ b/packages/backend/src/server/api/private/signup.ts
@@ -6,10 +6,8 @@ import { Users, RegistrationTickets, UserPendings } from "@/models/index.js";
 import { signup } from "@/server/api/common/signup.js";
 import config from "@/config/index.js";
 import { sendEmail } from "@/services/send-email.js";
-import { genId } from "backend-rs";
+import { genId, hashPassword } from "backend-rs";
 import { validateEmailForAccount } from "@/services/validate-email-for-account.js";
-import { hashPassword } from "@/misc/password.js";
-import { inspect } from "node:util";
 
 export default async (ctx: Koa.Context) => {
 	const body = ctx.request.body;
@@ -85,7 +83,7 @@ export default async (ctx: Koa.Context) => {
 		const code = rndstr("a-z0-9", 16);
 
 		// Generate hash of password
-		const hash = await hashPassword(password);
+		const hash = hashPassword(password);
 
 		await UserPendings.insert({
 			id: genId(),
diff --git a/packages/backend/src/services/create-system-user.ts b/packages/backend/src/services/create-system-user.ts
index 345de7ad17..802c59b288 100644
--- a/packages/backend/src/services/create-system-user.ts
+++ b/packages/backend/src/services/create-system-user.ts
@@ -4,17 +4,16 @@ import { genRsaKeyPair } from "@/misc/gen-key-pair.js";
 import { User } from "@/models/entities/user.js";
 import { UserProfile } from "@/models/entities/user-profile.js";
 import { IsNull } from "typeorm";
-import { genId } from "backend-rs";
+import { genId, hashPassword } from "backend-rs";
 import { UserKeypair } from "@/models/entities/user-keypair.js";
 import { UsedUsername } from "@/models/entities/used-username.js";
 import { db } from "@/db/postgre.js";
-import { hashPassword } from "@/misc/password.js";
 
 export async function createSystemUser(username: string) {
 	const password = uuid();
 
 	// Generate hash of password
-	const hash = await hashPassword(password);
+	const hash = hashPassword(password);
 
 	// Generate secret
 	const secret = generateNativeUserToken();
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 192b5f95e5..02b473ddce 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -99,9 +99,6 @@ importers:
       archiver:
         specifier: 7.0.1
         version: 7.0.1
-      argon2:
-        specifier: ^0.40.1
-        version: 0.40.1
       aws-sdk:
         specifier: 2.1597.0
         version: 2.1597.0
@@ -111,9 +108,6 @@ importers:
       backend-rs:
         specifier: workspace:*
         version: link:../backend-rs
-      bcryptjs:
-        specifier: 2.4.3
-        version: 2.4.3
       blurhash:
         specifier: 2.0.5
         version: 2.0.5
@@ -377,9 +371,6 @@ importers:
       '@types/adm-zip':
         specifier: ^0.5.5
         version: 0.5.5
-      '@types/bcryptjs':
-        specifier: 2.4.6
-        version: 2.4.6
       '@types/color-convert':
         specifier: ^2.0.3
         version: 2.0.3
@@ -3294,11 +3285,6 @@ packages:
       sshpk: 1.17.0
     dev: false
 
-  /@phc/format@1.0.0:
-    resolution: {integrity: sha512-m7X9U6BG2+J+R1lSOdCiITLLrxm+cWlNI3HUFA92oLO77ObGNzaKdh8pMLqdZcshtkKuV84olNNXDfMc4FezBQ==}
-    engines: {node: '>=10'}
-    dev: false
-
   /@phosphor-icons/web@2.1.1:
     resolution: {integrity: sha512-QjrfbItu5Rb2i37GzsKxmrRHfZPTVk3oXSPBnQ2+oACDbQRWGAeB0AsvZw263n1nFouQuff+khOCtRbrc6+k+A==}
     dev: true
@@ -3831,10 +3817,6 @@ packages:
       '@babel/types': 7.23.0
     dev: true
 
-  /@types/bcryptjs@2.4.6:
-    resolution: {integrity: sha512-9xlo6R2qDs5uixm0bcIqCeMCE6HiQsIyel9KQySStiyqNl2tnj2mP3DX1Nf56MD6KMenNNlBBsy3LJ7gUEQPXQ==}
-    dev: true
-
   /@types/body-parser@1.19.2:
     resolution: {integrity: sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g==}
     dependencies:
@@ -5358,16 +5340,6 @@ packages:
   /arg@4.1.3:
     resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==}
 
-  /argon2@0.40.1:
-    resolution: {integrity: sha512-DjtHDwd7pm12qeWyfihHoM8Bn5vGcgH6sKwgPqwNYroRmxlrzadHEvMyuvQxN/V8YSyRRKD5x6ito09q1e9OyA==}
-    engines: {node: '>=16.17.0'}
-    requiresBuild: true
-    dependencies:
-      '@phc/format': 1.0.0
-      node-addon-api: 7.1.0
-      node-gyp-build: 4.8.0
-    dev: false
-
   /argparse@1.0.10:
     resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==}
     dependencies:
@@ -5893,10 +5865,6 @@ packages:
       tweetnacl: 0.14.5
     dev: false
 
-  /bcryptjs@2.4.3:
-    resolution: {integrity: sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==}
-    dev: false
-
   /bin-check@4.1.0:
     resolution: {integrity: sha512-b6weQyEUKsDGFlACWSIOfveEnImkJyK/FGW6FAG42loyoquvjdtOIqO6yBFzHyqyVVhNgNkQxxx09SFLK28YnA==}
     engines: {node: '>=4'}
@@ -13029,11 +12997,6 @@ packages:
     dev: true
     optional: true
 
-  /node-addon-api@7.1.0:
-    resolution: {integrity: sha512-mNcltoe1R8o7STTegSOHdnJNN7s5EUvhoS7ShnTHDyOSd+8H+UdWODq6qSv67PjC8Zc5JRT8+oLAMCr0SIXw7g==}
-    engines: {node: ^16 || ^18 || >= 20}
-    dev: false
-
   /node-domexception@1.0.0:
     resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==}
     engines: {node: '>=10.5.0'}
@@ -13083,6 +13046,7 @@ packages:
   /node-gyp-build@4.8.0:
     resolution: {integrity: sha512-u6fs2AEUljNho3EYTJNBfImO5QTo/J/1Etd+NVdCj7qWKUSN/bSLkZwhDv7I+w/MSC6qJ4cknepkAYykDdK8og==}
     hasBin: true
+    dev: true
 
   /node-int64@0.4.0:
     resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==}

From ceca260c922f40318279cf2871da3a8f718ae4df Mon Sep 17 00:00:00 2001
From: naskya <m@naskya.net>
Date: Sun, 14 Apr 2024 14:53:25 +0900
Subject: [PATCH 056/110] refactor (backend): port convert-milliseconds to
 backend-rs

---
 packages/backend-rs/index.d.ts                |  2 +
 packages/backend-rs/index.js                  |  3 +-
 .../src/misc/format_milliseconds.rs           | 46 +++++++++++++++++++
 packages/backend-rs/src/misc/mod.rs           |  1 +
 .../backend/src/misc/convert-milliseconds.ts  | 17 -------
 packages/backend/src/server/api/limiter.ts    |  4 +-
 6 files changed, 53 insertions(+), 20 deletions(-)
 create mode 100644 packages/backend-rs/src/misc/format_milliseconds.rs
 delete mode 100644 packages/backend/src/misc/convert-milliseconds.ts

diff --git a/packages/backend-rs/index.d.ts b/packages/backend-rs/index.d.ts
index f2830dcac3..d3bb13164a 100644
--- a/packages/backend-rs/index.d.ts
+++ b/packages/backend-rs/index.d.ts
@@ -132,6 +132,8 @@ export function isSelfHost(host?: string | undefined | null): boolean
 export function isSameOrigin(uri: string): boolean
 export function extractHost(uri: string): string
 export function toPuny(host: string): string
+/** Convert milliseconds to a human readable string */
+export function formatMilliseconds(milliseconds: number): string
 export function toMastodonId(firefishId: string): string | null
 export function fromMastodonId(mastodonId: string): string | null
 export function nyaify(text: string, lang?: string | undefined | null): string
diff --git a/packages/backend-rs/index.js b/packages/backend-rs/index.js
index af9a293bbb..6fbfe9414d 100644
--- a/packages/backend-rs/index.js
+++ b/packages/backend-rs/index.js
@@ -310,7 +310,7 @@ if (!nativeBinding) {
   throw new Error(`Failed to load native binding`)
 }
 
-const { readServerConfig, stringToAcct, acctToString, checkWordMute, getFullApAccount, isSelfHost, isSameOrigin, extractHost, toPuny, toMastodonId, fromMastodonId, nyaify, hashPassword, verifyPassword, isOldPasswordAlgorithm, AntennaSrcEnum, MutedNoteReasonEnum, NoteVisibilityEnum, NotificationTypeEnum, PageVisibilityEnum, PollNotevisibilityEnum, RelayStatusEnum, UserEmojimodpermEnum, UserProfileFfvisibilityEnum, UserProfileMutingnotificationtypesEnum, initIdGenerator, getTimestamp, genId, secureRndstr } = nativeBinding
+const { readServerConfig, stringToAcct, acctToString, checkWordMute, getFullApAccount, isSelfHost, isSameOrigin, extractHost, toPuny, formatMilliseconds, toMastodonId, fromMastodonId, nyaify, hashPassword, verifyPassword, isOldPasswordAlgorithm, AntennaSrcEnum, MutedNoteReasonEnum, NoteVisibilityEnum, NotificationTypeEnum, PageVisibilityEnum, PollNotevisibilityEnum, RelayStatusEnum, UserEmojimodpermEnum, UserProfileFfvisibilityEnum, UserProfileMutingnotificationtypesEnum, initIdGenerator, getTimestamp, genId, secureRndstr } = nativeBinding
 
 module.exports.readServerConfig = readServerConfig
 module.exports.stringToAcct = stringToAcct
@@ -321,6 +321,7 @@ module.exports.isSelfHost = isSelfHost
 module.exports.isSameOrigin = isSameOrigin
 module.exports.extractHost = extractHost
 module.exports.toPuny = toPuny
+module.exports.formatMilliseconds = formatMilliseconds
 module.exports.toMastodonId = toMastodonId
 module.exports.fromMastodonId = fromMastodonId
 module.exports.nyaify = nyaify
diff --git a/packages/backend-rs/src/misc/format_milliseconds.rs b/packages/backend-rs/src/misc/format_milliseconds.rs
new file mode 100644
index 0000000000..dfa8df6f62
--- /dev/null
+++ b/packages/backend-rs/src/misc/format_milliseconds.rs
@@ -0,0 +1,46 @@
+/// Convert milliseconds to a human readable string
+#[crate::export]
+pub fn format_milliseconds(milliseconds: u32) -> String {
+    let mut seconds = milliseconds / 1000;
+    let mut minutes = seconds / 60;
+    let mut hours = minutes / 60;
+    let days = hours / 24;
+
+    seconds %= 60;
+    minutes %= 60;
+    hours %= 24;
+
+    let mut buf: Vec<String> = vec![];
+
+    if days > 0 {
+        buf.push(format!("{} day(s)", days));
+    }
+    if hours > 0 {
+        buf.push(format!("{} hour(s)", hours));
+    }
+    if minutes > 0 {
+        buf.push(format!("{} minute(s)", minutes));
+    }
+    if seconds > 0 {
+        buf.push(format!("{} second(s)", seconds));
+    }
+
+    buf.join(", ")
+}
+
+#[cfg(test)]
+mod unit_test {
+    use super::format_milliseconds;
+    use pretty_assertions::assert_eq;
+
+    #[test]
+    fn format_milliseconds_test() {
+        assert_eq!(format_milliseconds(1000), "1 second(s)");
+        assert_eq!(format_milliseconds(1387938), "23 minute(s), 7 second(s)");
+        assert_eq!(format_milliseconds(34200457), "9 hour(s), 30 minute(s)");
+        assert_eq!(
+            format_milliseconds(998244353),
+            "11 day(s), 13 hour(s), 17 minute(s), 24 second(s)"
+        );
+    }
+}
diff --git a/packages/backend-rs/src/misc/mod.rs b/packages/backend-rs/src/misc/mod.rs
index 6eda4e449e..d5a84e56a1 100644
--- a/packages/backend-rs/src/misc/mod.rs
+++ b/packages/backend-rs/src/misc/mod.rs
@@ -1,6 +1,7 @@
 pub mod acct;
 pub mod check_word_mute;
 pub mod convert_host;
+pub mod format_milliseconds;
 pub mod mastodon_id;
 pub mod nyaify;
 pub mod password;
diff --git a/packages/backend/src/misc/convert-milliseconds.ts b/packages/backend/src/misc/convert-milliseconds.ts
deleted file mode 100644
index d8c163ffda..0000000000
--- a/packages/backend/src/misc/convert-milliseconds.ts
+++ /dev/null
@@ -1,17 +0,0 @@
-export function convertMilliseconds(ms: number) {
-	let seconds = Math.round(ms / 1000);
-	let minutes = Math.round(seconds / 60);
-	let hours = Math.round(minutes / 60);
-	const days = Math.round(hours / 24);
-	seconds %= 60;
-	minutes %= 60;
-	hours %= 24;
-
-	const result = [];
-	if (days > 0) result.push(`${days} day(s)`);
-	if (hours > 0) result.push(`${hours} hour(s)`);
-	if (minutes > 0) result.push(`${minutes} minute(s)`);
-	if (seconds > 0) result.push(`${seconds} second(s)`);
-
-	return result.join(", ");
-}
diff --git a/packages/backend/src/server/api/limiter.ts b/packages/backend/src/server/api/limiter.ts
index f03f8754cf..d4a9353ade 100644
--- a/packages/backend/src/server/api/limiter.ts
+++ b/packages/backend/src/server/api/limiter.ts
@@ -2,7 +2,7 @@ import Limiter from "ratelimiter";
 import Logger from "@/services/logger.js";
 import { redisClient } from "@/db/redis.js";
 import type { IEndpointMeta } from "./endpoints.js";
-import { convertMilliseconds } from "@/misc/convert-milliseconds.js";
+import { formatMilliseconds } from "backend-rs";
 
 const logger = new Logger("limiter");
 
@@ -78,7 +78,7 @@ export const limiter = (
 				if (info.remaining === 0) {
 					reject({
 						message: "RATE_LIMIT_EXCEEDED",
-						remainingTime: convertMilliseconds(info.resetMs - Date.now()),
+						remainingTime: formatMilliseconds(info.resetMs - Date.now()),
 					});
 				} else {
 					ok();

From b71da18b032d3cf6a6593e648c9fe9ac04726f29 Mon Sep 17 00:00:00 2001
From: sup39 <dev@sup39.dev>
Date: Sun, 14 Apr 2024 20:16:22 +0900
Subject: [PATCH 057/110] refactor (backend): port fetch-meta to backend-rs

Co-authored-by: naskya <m@naskya.net>
---
 packages/backend-rs/index.d.ts                | 13 +++
 packages/backend-rs/index.js                  |  4 +-
 packages/backend-rs/src/misc/meta.rs          | 83 +++++++++++++++++++
 packages/backend-rs/src/misc/mod.rs           |  1 +
 packages/backend/src/daemons/server-stats.ts  |  4 +-
 packages/backend/src/misc/fetch-meta.ts       | 72 ----------------
 .../backend/src/misc/fetch-proxy-account.ts   |  4 +-
 packages/backend/src/misc/reaction-lib.ts     |  6 +-
 .../backend/src/misc/should-block-instance.ts |  6 +-
 .../backend/src/misc/skipped-instances.ts     |  4 +-
 packages/backend/src/misc/translate.ts        |  4 +-
 .../backend/src/queue/processors/inbox.ts     |  4 +-
 .../src/remote/activitypub/check-fetch.ts     |  6 +-
 .../src/remote/activitypub/models/image.ts    |  4 +-
 .../src/remote/activitypub/resolver.ts        |  4 +-
 packages/backend/src/server/activitypub.ts    | 21 +++--
 .../src/server/activitypub/featured.ts        |  4 +-
 .../src/server/activitypub/followers.ts       |  4 +-
 .../src/server/activitypub/following.ts       |  4 +-
 .../backend/src/server/activitypub/outbox.ts  |  4 +-
 .../backend/src/server/api/api-handler.ts     |  4 +-
 packages/backend/src/server/api/call.ts       |  4 +-
 .../src/server/api/endpoints/admin/meta.ts    |  4 +-
 .../src/server/api/endpoints/custom-motd.ts   |  4 +-
 .../api/endpoints/custom-splash-icons.ts      |  4 +-
 .../backend/src/server/api/endpoints/drive.ts |  4 +-
 .../api/endpoints/drive/files/create.ts       |  4 +-
 .../api/endpoints/federation/instances.ts     |  6 +-
 .../server/api/endpoints/hashtags/trend.ts    |  4 +-
 .../server/api/endpoints/i/import-posts.ts    |  4 +-
 .../backend/src/server/api/endpoints/meta.ts  |  4 +-
 .../api/endpoints/notes/global-timeline.ts    |  4 +-
 .../api/endpoints/notes/hybrid-timeline.ts    |  4 +-
 .../api/endpoints/notes/local-timeline.ts     |  4 +-
 .../endpoints/notes/recommended-timeline.ts   |  4 +-
 .../src/server/api/endpoints/pinned-users.ts  |  4 +-
 .../api/endpoints/recommended-instances.ts    |  4 +-
 .../src/server/api/endpoints/server-info.ts   |  4 +-
 .../src/server/api/endpoints/sw/register.ts   |  4 +-
 .../api/endpoints/users/report-abuse.ts       |  4 +-
 .../src/server/api/mastodon/endpoints/meta.ts |  4 +-
 .../server/api/mastodon/endpoints/status.ts   |  7 +-
 .../backend/src/server/api/private/signup.ts  |  5 +-
 .../api/stream/channels/global-timeline.ts    |  5 +-
 .../api/stream/channels/hybrid-timeline.ts    |  5 +-
 .../api/stream/channels/local-timeline.ts     |  5 +-
 .../stream/channels/recommended-timeline.ts   |  7 +-
 packages/backend/src/server/index.ts          |  4 +-
 packages/backend/src/server/nodeinfo.ts       |  4 +-
 packages/backend/src/server/web/index.ts      | 21 +++--
 packages/backend/src/server/web/manifest.ts   |  4 +-
 .../backend/src/server/web/url-preview.ts     |  4 +-
 .../backend/src/services/drive/add-file.ts    | 10 +--
 .../backend/src/services/drive/delete-file.ts |  4 +-
 .../backend/src/services/push-notification.ts |  4 +-
 packages/backend/src/services/send-email.ts   |  4 +-
 .../services/validate-email-for-account.ts    |  4 +-
 57 files changed, 229 insertions(+), 210 deletions(-)
 create mode 100644 packages/backend-rs/src/misc/meta.rs
 delete mode 100644 packages/backend/src/misc/fetch-meta.ts

diff --git a/packages/backend-rs/index.d.ts b/packages/backend-rs/index.d.ts
index d3bb13164a..3770056d8f 100644
--- a/packages/backend-rs/index.d.ts
+++ b/packages/backend-rs/index.d.ts
@@ -136,6 +136,19 @@ export function toPuny(host: string): string
 export function formatMilliseconds(milliseconds: number): string
 export function toMastodonId(firefishId: string): string | null
 export function fromMastodonId(mastodonId: string): string | null
+export function fetchMeta(useCache: boolean): Promise<Meta>
+export interface PugArgs {
+  img: string | null
+  title: string
+  instanceName: string
+  desc: string | null
+  icon: string | null
+  splashIcon: string | null
+  themeColor: string | null
+  randomMotd: string
+  privateMode: boolean | null
+}
+export function metaToPugArgs(meta: Meta): PugArgs
 export function nyaify(text: string, lang?: string | undefined | null): string
 export function hashPassword(password: string): string
 export function verifyPassword(password: string, hash: string): boolean
diff --git a/packages/backend-rs/index.js b/packages/backend-rs/index.js
index 6fbfe9414d..363c858e4a 100644
--- a/packages/backend-rs/index.js
+++ b/packages/backend-rs/index.js
@@ -310,7 +310,7 @@ if (!nativeBinding) {
   throw new Error(`Failed to load native binding`)
 }
 
-const { readServerConfig, stringToAcct, acctToString, checkWordMute, getFullApAccount, isSelfHost, isSameOrigin, extractHost, toPuny, formatMilliseconds, toMastodonId, fromMastodonId, nyaify, hashPassword, verifyPassword, isOldPasswordAlgorithm, AntennaSrcEnum, MutedNoteReasonEnum, NoteVisibilityEnum, NotificationTypeEnum, PageVisibilityEnum, PollNotevisibilityEnum, RelayStatusEnum, UserEmojimodpermEnum, UserProfileFfvisibilityEnum, UserProfileMutingnotificationtypesEnum, initIdGenerator, getTimestamp, genId, secureRndstr } = nativeBinding
+const { readServerConfig, stringToAcct, acctToString, checkWordMute, getFullApAccount, isSelfHost, isSameOrigin, extractHost, toPuny, formatMilliseconds, toMastodonId, fromMastodonId, fetchMeta, metaToPugArgs, nyaify, hashPassword, verifyPassword, isOldPasswordAlgorithm, AntennaSrcEnum, MutedNoteReasonEnum, NoteVisibilityEnum, NotificationTypeEnum, PageVisibilityEnum, PollNotevisibilityEnum, RelayStatusEnum, UserEmojimodpermEnum, UserProfileFfvisibilityEnum, UserProfileMutingnotificationtypesEnum, initIdGenerator, getTimestamp, genId, secureRndstr } = nativeBinding
 
 module.exports.readServerConfig = readServerConfig
 module.exports.stringToAcct = stringToAcct
@@ -324,6 +324,8 @@ module.exports.toPuny = toPuny
 module.exports.formatMilliseconds = formatMilliseconds
 module.exports.toMastodonId = toMastodonId
 module.exports.fromMastodonId = fromMastodonId
+module.exports.fetchMeta = fetchMeta
+module.exports.metaToPugArgs = metaToPugArgs
 module.exports.nyaify = nyaify
 module.exports.hashPassword = hashPassword
 module.exports.verifyPassword = verifyPassword
diff --git a/packages/backend-rs/src/misc/meta.rs b/packages/backend-rs/src/misc/meta.rs
new file mode 100644
index 0000000000..5aed617038
--- /dev/null
+++ b/packages/backend-rs/src/misc/meta.rs
@@ -0,0 +1,83 @@
+use crate::database::db_conn;
+use crate::model::entity::meta;
+use rand::prelude::*;
+use sea_orm::{prelude::*, ActiveValue};
+use std::sync::Mutex;
+
+type Meta = meta::Model;
+
+static CACHE: Mutex<Option<Meta>> = Mutex::new(None);
+fn update_cache(meta: &Meta) {
+    let _ = CACHE.lock().map(|mut cache| *cache = Some(meta.clone()));
+}
+
+#[crate::export]
+pub async fn fetch_meta(use_cache: bool) -> Result<Meta, DbErr> {
+    // try using cache
+    if use_cache {
+        if let Some(cache) = CACHE.lock().ok().and_then(|cache| cache.clone()) {
+            return Ok(cache);
+        }
+    }
+
+    // try fetching from db
+    let db = db_conn().await?;
+    let meta = meta::Entity::find().one(db).await?;
+    if let Some(meta) = meta {
+        update_cache(&meta);
+        return Ok(meta);
+    }
+
+    // create a new meta object and insert into db
+    let meta = meta::Entity::insert(meta::ActiveModel {
+        id: ActiveValue::Set("x".to_owned()),
+        ..Default::default()
+    })
+    .exec_with_returning(db)
+    .await?;
+    update_cache(&meta);
+    Ok(meta)
+}
+
+#[crate::export(object)]
+pub struct PugArgs {
+    pub img: Option<String>,
+    pub title: String,
+    pub instance_name: String,
+    pub desc: Option<String>,
+    pub icon: Option<String>,
+    pub splash_icon: Option<String>,
+    pub theme_color: Option<String>,
+    pub random_motd: String,
+    pub private_mode: Option<bool>,
+}
+
+#[crate::export]
+pub fn meta_to_pug_args(meta: Meta) -> PugArgs {
+    let mut rng = rand::thread_rng();
+
+    let splash_icon = meta
+        .custom_splash_icons
+        .choose(&mut rng)
+        .map(|s| s.to_owned())
+        .or_else(|| meta.icon_url.to_owned());
+
+    let random_motd = meta
+        .custom_motd
+        .choose(&mut rng)
+        .map(|s| s.to_owned())
+        .unwrap_or_else(|| "Loading...".to_owned());
+
+    let name = meta.name.unwrap_or_else(|| "Firefish".to_owned());
+    PugArgs {
+        img: meta.banner_url,
+        title: name.clone(),
+        instance_name: name.clone(),
+        desc: meta.description,
+        icon: meta.icon_url,
+        splash_icon,
+        theme_color: meta.theme_color,
+        random_motd,
+        private_mode: meta.private_mode,
+    }
+}
diff --git a/packages/backend-rs/src/misc/mod.rs b/packages/backend-rs/src/misc/mod.rs
index d5a84e56a1..7f99a67324 100644
--- a/packages/backend-rs/src/misc/mod.rs
+++ b/packages/backend-rs/src/misc/mod.rs
@@ -3,5 +3,6 @@ pub mod check_word_mute;
 pub mod convert_host;
 pub mod format_milliseconds;
 pub mod mastodon_id;
+pub mod meta;
 pub mod nyaify;
 pub mod password;
diff --git a/packages/backend/src/daemons/server-stats.ts b/packages/backend/src/daemons/server-stats.ts
index 58a9b1491b..92265ecba7 100644
--- a/packages/backend/src/daemons/server-stats.ts
+++ b/packages/backend/src/daemons/server-stats.ts
@@ -1,7 +1,7 @@
 import si from "systeminformation";
 import Xev from "xev";
 import * as osUtils from "os-utils";
-import { fetchMeta } from "@/misc/fetch-meta.js";
+import { fetchMeta } from "backend-rs";
 
 const ev = new Xev();
 
@@ -20,7 +20,7 @@ export default function () {
 		ev.emit(`serverStatsLog:${x.id}`, log.slice(0, x.length || 50));
 	});
 
-	fetchMeta().then((meta) => {
+	fetchMeta(true).then((meta) => {
 		if (!meta.enableServerMachineStats) return;
 	});
 
diff --git a/packages/backend/src/misc/fetch-meta.ts b/packages/backend/src/misc/fetch-meta.ts
deleted file mode 100644
index fdc978b5a3..0000000000
--- a/packages/backend/src/misc/fetch-meta.ts
+++ /dev/null
@@ -1,72 +0,0 @@
-import { db } from "@/db/postgre.js";
-import { Meta } from "@/models/entities/meta.js";
-
-let cache: Meta;
-
-export function metaToPugArgs(meta: Meta): object {
-	let motd = ["Loading..."];
-	if (meta.customMotd.length > 0) {
-		motd = meta.customMotd;
-	}
-	let splashIconUrl = meta.iconUrl;
-	if (meta.customSplashIcons.length > 0) {
-		splashIconUrl =
-			meta.customSplashIcons[
-				Math.floor(Math.random() * meta.customSplashIcons.length)
-			];
-	}
-
-	return {
-		img: meta.bannerUrl,
-		title: meta.name || "Firefish",
-		instanceName: meta.name || "Firefish",
-		desc: meta.description,
-		icon: meta.iconUrl,
-		splashIcon: splashIconUrl,
-		themeColor: meta.themeColor,
-		randomMOTD: motd[Math.floor(Math.random() * motd.length)],
-		privateMode: meta.privateMode,
-	};
-}
-
-export async function fetchMeta(noCache = false): Promise<Meta> {
-	if (!noCache && cache) return cache;
-
-	return await db.transaction(async (transactionalEntityManager) => {
-		// New IDs are prioritized because multiple records may have been created due to past bugs.
-		const metas = await transactionalEntityManager.find(Meta, {
-			order: {
-				id: "DESC",
-			},
-		});
-
-		const meta = metas[0];
-
-		if (meta) {
-			cache = meta;
-			return meta;
-		} else {
-			// If fetchMeta is called at the same time when meta is empty, this part may be called at the same time, so use fail-safe upsert.
-			const saved = await transactionalEntityManager
-				.upsert(
-					Meta,
-					{
-						id: "x",
-					},
-					["id"],
-				)
-				.then((x) =>
-					transactionalEntityManager.findOneByOrFail(Meta, x.identifiers[0]),
-				);
-
-			cache = saved;
-			return saved;
-		}
-	});
-}
-
-setInterval(() => {
-	fetchMeta(true).then((meta) => {
-		cache = meta;
-	});
-}, 1000 * 10);
diff --git a/packages/backend/src/misc/fetch-proxy-account.ts b/packages/backend/src/misc/fetch-proxy-account.ts
index a277db6fb9..8d015da25d 100644
--- a/packages/backend/src/misc/fetch-proxy-account.ts
+++ b/packages/backend/src/misc/fetch-proxy-account.ts
@@ -1,9 +1,9 @@
-import { fetchMeta } from "./fetch-meta.js";
+import { fetchMeta } from "backend-rs";
 import type { ILocalUser } from "@/models/entities/user.js";
 import { Users } from "@/models/index.js";
 
 export async function fetchProxyAccount(): Promise<ILocalUser | null> {
-	const meta = await fetchMeta();
+	const meta = await fetchMeta(true);
 	if (meta.proxyAccountId == null) return null;
 	return (await Users.findOneByOrFail({
 		id: meta.proxyAccountId,
diff --git a/packages/backend/src/misc/reaction-lib.ts b/packages/backend/src/misc/reaction-lib.ts
index 4304d3b9e1..691d9743c8 100644
--- a/packages/backend/src/misc/reaction-lib.ts
+++ b/packages/backend/src/misc/reaction-lib.ts
@@ -1,5 +1,5 @@
 import { emojiRegex } from "./emoji-regex.js";
-import { fetchMeta } from "./fetch-meta.js";
+import { fetchMeta } from "backend-rs";
 import { Emojis } from "@/models/index.js";
 import { toPuny } from "backend-rs";
 import { IsNull } from "typeorm";
@@ -21,7 +21,7 @@ export async function toDbReaction(
 	reaction?: string | null,
 	reacterHost?: string | null,
 ): Promise<string> {
-	if (!reaction) return (await fetchMeta()).defaultReaction;
+	if (!reaction) return (await fetchMeta(true)).defaultReaction;
 
 	reacterHost = reacterHost == null ? null : toPuny(reacterHost);
 
@@ -45,7 +45,7 @@ export async function toDbReaction(
 		if (emoji) return reacterHost ? `:${name}@${reacterHost}:` : `:${name}:`;
 	}
 
-	return (await fetchMeta()).defaultReaction;
+	return (await fetchMeta(true)).defaultReaction;
 }
 
 type DecodedReaction = {
diff --git a/packages/backend/src/misc/should-block-instance.ts b/packages/backend/src/misc/should-block-instance.ts
index 35ed307931..465be41f2a 100644
--- a/packages/backend/src/misc/should-block-instance.ts
+++ b/packages/backend/src/misc/should-block-instance.ts
@@ -1,4 +1,4 @@
-import { fetchMeta } from "@/misc/fetch-meta.js";
+import { fetchMeta } from "backend-rs";
 import type { Instance } from "@/models/entities/instance.js";
 import type { Meta } from "@/models/entities/meta.js";
 
@@ -13,7 +13,7 @@ export async function shouldBlockInstance(
 	host: Instance["host"],
 	meta?: Meta,
 ): Promise<boolean> {
-	const { blockedHosts } = meta ?? (await fetchMeta());
+	const { blockedHosts } = meta ?? (await fetchMeta(true));
 	return blockedHosts.some(
 		(blockedHost) => host === blockedHost || host.endsWith(`.${blockedHost}`),
 	);
@@ -30,7 +30,7 @@ export async function shouldSilenceInstance(
 	host: Instance["host"],
 	meta?: Meta,
 ): Promise<boolean> {
-	const { silencedHosts } = meta ?? (await fetchMeta());
+	const { silencedHosts } = meta ?? (await fetchMeta(true));
 	return silencedHosts.some(
 		(silencedHost) =>
 			host === silencedHost || host.endsWith(`.${silencedHost}`),
diff --git a/packages/backend/src/misc/skipped-instances.ts b/packages/backend/src/misc/skipped-instances.ts
index 785393022a..14b26a3032 100644
--- a/packages/backend/src/misc/skipped-instances.ts
+++ b/packages/backend/src/misc/skipped-instances.ts
@@ -1,5 +1,5 @@
 import { Brackets } from "typeorm";
-import { fetchMeta } from "@/misc/fetch-meta.js";
+import { fetchMeta } from "backend-rs";
 import { Instances } from "@/models/index.js";
 import type { Instance } from "@/models/entities/instance.js";
 import { DAY } from "@/const.js";
@@ -19,7 +19,7 @@ export async function skippedInstances(
 	hosts: Instance["host"][],
 ): Promise<Instance["host"][]> {
 	// first check for blocked instances since that info may already be in memory
-	const meta = await fetchMeta();
+	const meta = await fetchMeta(true);
 	const shouldSkip = await Promise.all(
 		hosts.map((host) => shouldBlockInstance(host, meta)),
 	);
diff --git a/packages/backend/src/misc/translate.ts b/packages/backend/src/misc/translate.ts
index e622171ec6..3395ce93be 100644
--- a/packages/backend/src/misc/translate.ts
+++ b/packages/backend/src/misc/translate.ts
@@ -1,7 +1,7 @@
 import fetch from "node-fetch";
 import { Converter } from "opencc-js";
 import { getAgentByUrl } from "@/misc/fetch.js";
-import { fetchMeta } from "@/misc/fetch-meta.js";
+import { fetchMeta } from "backend-rs";
 import type { PostLanguage } from "@/misc/langmap";
 import * as deepl from "deepl-node";
 
@@ -26,7 +26,7 @@ export async function translate(
 	from: PostLanguage | null,
 	to: PostLanguage,
 ) {
-	const instance = await fetchMeta();
+	const instance = await fetchMeta(true);
 
 	if (instance.deeplAuthKey == null && instance.libreTranslateApiUrl == null) {
 		throw Error("No translator is set up on this server.");
diff --git a/packages/backend/src/queue/processors/inbox.ts b/packages/backend/src/queue/processors/inbox.ts
index 46f9939a41..0ea72306b6 100644
--- a/packages/backend/src/queue/processors/inbox.ts
+++ b/packages/backend/src/queue/processors/inbox.ts
@@ -5,7 +5,7 @@ import perform from "@/remote/activitypub/perform.js";
 import Logger from "@/services/logger.js";
 import { registerOrFetchInstanceDoc } from "@/services/register-or-fetch-instance-doc.js";
 import { Instances } from "@/models/index.js";
-import { fetchMeta } from "@/misc/fetch-meta.js";
+import { fetchMeta } from "backend-rs";
 import { toPuny, extractHost } from "backend-rs";
 import { getApId } from "@/remote/activitypub/type.js";
 import { fetchInstanceMetadata } from "@/services/fetch-instance-metadata.js";
@@ -41,7 +41,7 @@ export default async (job: Bull.Job<InboxJobData>): Promise<string> => {
 	const host = toPuny(new URL(signature.keyId).hostname);
 
 	// interrupt if blocked
-	const meta = await fetchMeta();
+	const meta = await fetchMeta(true);
 	if (await shouldBlockInstance(host, meta)) {
 		return `Blocked request: ${host}`;
 	}
diff --git a/packages/backend/src/remote/activitypub/check-fetch.ts b/packages/backend/src/remote/activitypub/check-fetch.ts
index c0d40278ee..12ea63a931 100644
--- a/packages/backend/src/remote/activitypub/check-fetch.ts
+++ b/packages/backend/src/remote/activitypub/check-fetch.ts
@@ -1,7 +1,7 @@
 import { URL } from "url";
 import httpSignature, { IParsedSignature } from "@peertube/http-signature";
 import config from "@/config/index.js";
-import { fetchMeta } from "@/misc/fetch-meta.js";
+import { fetchMeta } from "backend-rs";
 import { toPuny } from "backend-rs";
 import DbResolver from "@/remote/activitypub/db-resolver.js";
 import { getApId } from "@/remote/activitypub/type.js";
@@ -12,7 +12,7 @@ import type { UserPublickey } from "@/models/entities/user-publickey.js";
 import { verify } from "node:crypto";
 
 export async function hasSignature(req: IncomingMessage): Promise<string> {
-	const meta = await fetchMeta();
+	const meta = await fetchMeta(true);
 	const required = meta.secureMode || meta.privateMode;
 
 	try {
@@ -27,7 +27,7 @@ export async function hasSignature(req: IncomingMessage): Promise<string> {
 }
 
 export async function checkFetch(req: IncomingMessage): Promise<number> {
-	const meta = await fetchMeta();
+	const meta = await fetchMeta(true);
 	if (meta.secureMode || meta.privateMode) {
 		if (req.headers.host !== config.host) return 400;
 
diff --git a/packages/backend/src/remote/activitypub/models/image.ts b/packages/backend/src/remote/activitypub/models/image.ts
index 2cf0c6c152..e2072a963a 100644
--- a/packages/backend/src/remote/activitypub/models/image.ts
+++ b/packages/backend/src/remote/activitypub/models/image.ts
@@ -1,7 +1,7 @@
 import { uploadFromUrl } from "@/services/drive/upload-from-url.js";
 import type { CacheableRemoteUser } from "@/models/entities/user.js";
 import Resolver from "../resolver.js";
-import { fetchMeta } from "@/misc/fetch-meta.js";
+import { fetchMeta } from "backend-rs";
 import { apLogger } from "../logger.js";
 import type { DriveFile } from "@/models/entities/drive-file.js";
 import { DriveFiles } from "@/models/index.js";
@@ -34,7 +34,7 @@ export async function createImage(
 
 	logger.info(`Creating the Image: ${image.url}`);
 
-	const instance = await fetchMeta();
+	const instance = await fetchMeta(true);
 
 	let file = await uploadFromUrl({
 		url: image.url,
diff --git a/packages/backend/src/remote/activitypub/resolver.ts b/packages/backend/src/remote/activitypub/resolver.ts
index 973f12cdc2..79b7962b72 100644
--- a/packages/backend/src/remote/activitypub/resolver.ts
+++ b/packages/backend/src/remote/activitypub/resolver.ts
@@ -1,7 +1,7 @@
 import config from "@/config/index.js";
 import type { ILocalUser } from "@/models/entities/user.js";
 import { getInstanceActor } from "@/services/instance-actor.js";
-import { fetchMeta } from "@/misc/fetch-meta.js";
+import { fetchMeta } from "backend-rs";
 import { extractHost, isSelfHost } from "backend-rs";
 import { apGet } from "./request.js";
 import type { IObject, ICollection, IOrderedCollection } from "./type.js";
@@ -100,7 +100,7 @@ export default class Resolver {
 			return await this.resolveLocal(value);
 		}
 
-		const meta = await fetchMeta();
+		const meta = await fetchMeta(true);
 		if (await shouldBlockInstance(host, meta)) {
 			throw new Error("Instance is blocked");
 		}
diff --git a/packages/backend/src/server/activitypub.ts b/packages/backend/src/server/activitypub.ts
index 7e5fb5a281..00c8a6babe 100644
--- a/packages/backend/src/server/activitypub.ts
+++ b/packages/backend/src/server/activitypub.ts
@@ -9,7 +9,7 @@ import renderKey from "@/remote/activitypub/renderer/key.js";
 import { renderPerson } from "@/remote/activitypub/renderer/person.js";
 import renderEmoji from "@/remote/activitypub/renderer/emoji.js";
 import { inbox as processInbox } from "@/queue/index.js";
-import { isSelfHost } from "backend-rs";
+import { fetchMeta, isSelfHost } from "backend-rs";
 import {
 	Notes,
 	Users,
@@ -25,7 +25,6 @@ import {
 	getSignatureUser,
 } from "@/remote/activitypub/check-fetch.js";
 import { getInstanceActor } from "@/services/instance-actor.js";
-import { fetchMeta } from "@/misc/fetch-meta.js";
 import renderFollow from "@/remote/activitypub/renderer/follow.js";
 import Featured from "./activitypub/featured.js";
 import Following from "./activitypub/following.js";
@@ -238,7 +237,7 @@ router.get("/notes/:note", async (ctx, next) => {
 
 	ctx.body = renderActivity(await renderNote(note, false));
 
-	const meta = await fetchMeta();
+	const meta = await fetchMeta(true);
 	if (meta.secureMode || meta.privateMode) {
 		ctx.set("Cache-Control", "private, max-age=0, must-revalidate");
 	} else {
@@ -268,7 +267,7 @@ router.get("/notes/:note/activity", async (ctx) => {
 	}
 
 	ctx.body = renderActivity(await packActivity(note));
-	const meta = await fetchMeta();
+	const meta = await fetchMeta(true);
 	if (meta.secureMode || meta.privateMode) {
 		ctx.set("Cache-Control", "private, max-age=0, must-revalidate");
 	} else {
@@ -323,7 +322,7 @@ router.get("/users/:user/publickey", async (ctx) => {
 
 	if (Users.isLocalUser(user)) {
 		ctx.body = renderActivity(renderKey(user, keypair));
-		const meta = await fetchMeta();
+		const meta = await fetchMeta(true);
 		if (meta.secureMode || meta.privateMode) {
 			ctx.set("Cache-Control", "private, max-age=0, must-revalidate");
 		} else {
@@ -343,7 +342,7 @@ async function userInfo(ctx: Router.RouterContext, user: User | null) {
 	}
 
 	ctx.body = renderActivity(await renderPerson(user as ILocalUser));
-	const meta = await fetchMeta();
+	const meta = await fetchMeta(true);
 	if (meta.secureMode || meta.privateMode) {
 		ctx.set("Cache-Control", "private, max-age=0, must-revalidate");
 	} else {
@@ -426,8 +425,8 @@ router.get("/emojis/:emoji", async (ctx) => {
 		return;
 	}
 
-	ctx.body = renderActivity(await renderEmoji(emoji));
-	const meta = await fetchMeta();
+	ctx.body = renderActivity(renderEmoji(emoji));
+	const meta = await fetchMeta(true);
 	if (meta.secureMode || meta.privateMode) {
 		ctx.set("Cache-Control", "private, max-age=0, must-revalidate");
 	} else {
@@ -459,7 +458,7 @@ router.get("/likes/:like", async (ctx) => {
 	}
 
 	ctx.body = renderActivity(await renderLike(reaction, note));
-	const meta = await fetchMeta();
+	const meta = await fetchMeta(true);
 	if (meta.secureMode || meta.privateMode) {
 		ctx.set("Cache-Control", "private, max-age=0, must-revalidate");
 	} else {
@@ -497,7 +496,7 @@ router.get(
 		}
 
 		ctx.body = renderActivity(renderFollow(follower, followee));
-		const meta = await fetchMeta();
+		const meta = await fetchMeta(true);
 		if (meta.secureMode || meta.privateMode) {
 			ctx.set("Cache-Control", "private, max-age=0, must-revalidate");
 		} else {
@@ -540,7 +539,7 @@ router.get("/follows/:followRequestId", async (ctx: Router.RouterContext) => {
 		return;
 	}
 
-	const meta = await fetchMeta();
+	const meta = await fetchMeta(true);
 	if (meta.secureMode || meta.privateMode) {
 		ctx.set("Cache-Control", "private, max-age=0, must-revalidate");
 	} else {
diff --git a/packages/backend/src/server/activitypub/featured.ts b/packages/backend/src/server/activitypub/featured.ts
index 464a7f769d..e7ea6f238e 100644
--- a/packages/backend/src/server/activitypub/featured.ts
+++ b/packages/backend/src/server/activitypub/featured.ts
@@ -5,7 +5,7 @@ import renderOrderedCollection from "@/remote/activitypub/renderer/ordered-colle
 import renderNote from "@/remote/activitypub/renderer/note.js";
 import { Users, Notes, UserNotePinings } from "@/models/index.js";
 import { checkFetch } from "@/remote/activitypub/check-fetch.js";
-import { fetchMeta } from "@/misc/fetch-meta.js";
+import { fetchMeta } from "backend-rs";
 import { setResponseType } from "../activitypub.js";
 import type Router from "@koa/router";
 
@@ -57,7 +57,7 @@ export default async (ctx: Router.RouterContext) => {
 
 	ctx.body = renderActivity(rendered);
 
-	const meta = await fetchMeta();
+	const meta = await fetchMeta(true);
 	if (meta.secureMode || meta.privateMode) {
 		ctx.set("Cache-Control", "private, max-age=0, must-revalidate");
 	} else {
diff --git a/packages/backend/src/server/activitypub/followers.ts b/packages/backend/src/server/activitypub/followers.ts
index 3c9e5fa201..576a672d6d 100644
--- a/packages/backend/src/server/activitypub/followers.ts
+++ b/packages/backend/src/server/activitypub/followers.ts
@@ -8,7 +8,7 @@ import renderFollowUser from "@/remote/activitypub/renderer/follow-user.js";
 import { Users, Followings, UserProfiles } from "@/models/index.js";
 import type { Following } from "@/models/entities/following.js";
 import { checkFetch } from "@/remote/activitypub/check-fetch.js";
-import { fetchMeta } from "@/misc/fetch-meta.js";
+import { fetchMeta } from "backend-rs";
 import { setResponseType } from "../activitypub.js";
 import type { FindOptionsWhere } from "typeorm";
 import type Router from "@koa/router";
@@ -110,7 +110,7 @@ export default async (ctx: Router.RouterContext) => {
 		ctx.body = renderActivity(rendered);
 		setResponseType(ctx);
 	}
-	const meta = await fetchMeta();
+	const meta = await fetchMeta(true);
 	if (meta.secureMode || meta.privateMode) {
 		ctx.set("Cache-Control", "private, max-age=0, must-revalidate");
 	} else {
diff --git a/packages/backend/src/server/activitypub/following.ts b/packages/backend/src/server/activitypub/following.ts
index cfbe985911..76b4e79716 100644
--- a/packages/backend/src/server/activitypub/following.ts
+++ b/packages/backend/src/server/activitypub/following.ts
@@ -8,7 +8,7 @@ import renderFollowUser from "@/remote/activitypub/renderer/follow-user.js";
 import { Users, Followings, UserProfiles } from "@/models/index.js";
 import type { Following } from "@/models/entities/following.js";
 import { checkFetch } from "@/remote/activitypub/check-fetch.js";
-import { fetchMeta } from "@/misc/fetch-meta.js";
+import { fetchMeta } from "backend-rs";
 import { setResponseType } from "../activitypub.js";
 import type { FindOptionsWhere } from "typeorm";
 import type Router from "@koa/router";
@@ -110,7 +110,7 @@ export default async (ctx: Router.RouterContext) => {
 		ctx.body = renderActivity(rendered);
 		setResponseType(ctx);
 	}
-	const meta = await fetchMeta();
+	const meta = await fetchMeta(true);
 	if (meta.secureMode || meta.privateMode) {
 		ctx.set("Cache-Control", "private, max-age=0, must-revalidate");
 	} else {
diff --git a/packages/backend/src/server/activitypub/outbox.ts b/packages/backend/src/server/activitypub/outbox.ts
index 53aa6f4ad5..305102cf12 100644
--- a/packages/backend/src/server/activitypub/outbox.ts
+++ b/packages/backend/src/server/activitypub/outbox.ts
@@ -11,7 +11,7 @@ import * as url from "@/prelude/url.js";
 import { Users, Notes } from "@/models/index.js";
 import type { Note } from "@/models/entities/note.js";
 import { checkFetch } from "@/remote/activitypub/check-fetch.js";
-import { fetchMeta } from "@/misc/fetch-meta.js";
+import { fetchMeta } from "backend-rs";
 import { makePaginationQuery } from "../api/common/make-pagination-query.js";
 import { setResponseType } from "../activitypub.js";
 import type Router from "@koa/router";
@@ -117,7 +117,7 @@ export default async (ctx: Router.RouterContext) => {
 
 		setResponseType(ctx);
 	}
-	const meta = await fetchMeta();
+	const meta = await fetchMeta(true);
 	if (meta.secureMode || meta.privateMode) {
 		ctx.set("Cache-Control", "private, max-age=0, must-revalidate");
 	} else {
diff --git a/packages/backend/src/server/api/api-handler.ts b/packages/backend/src/server/api/api-handler.ts
index 620b754f30..5e65636427 100644
--- a/packages/backend/src/server/api/api-handler.ts
+++ b/packages/backend/src/server/api/api-handler.ts
@@ -2,7 +2,7 @@ import type Koa from "koa";
 
 import type { User } from "@/models/entities/user.js";
 import { UserIps } from "@/models/index.js";
-import { fetchMeta } from "@/misc/fetch-meta.js";
+import { fetchMeta } from "backend-rs";
 import type { IEndpoint } from "./endpoints.js";
 import authenticate, { AuthenticationError } from "./authenticate.js";
 import call from "./call.js";
@@ -84,7 +84,7 @@ export default (endpoint: IEndpoint, ctx: Koa.Context) =>
 
 				// Log IP
 				if (user) {
-					fetchMeta().then((meta) => {
+					fetchMeta(true).then((meta) => {
 						if (!meta.enableIpLogging) return;
 						const ip = ctx.ip;
 						const ips = userIpHistories.get(user.id);
diff --git a/packages/backend/src/server/api/call.ts b/packages/backend/src/server/api/call.ts
index 2faef7b0e8..3107156a9b 100644
--- a/packages/backend/src/server/api/call.ts
+++ b/packages/backend/src/server/api/call.ts
@@ -10,7 +10,7 @@ import endpoints from "./endpoints.js";
 import compatibility from "./compatibility.js";
 import { ApiError } from "./error.js";
 import { apiLogger } from "./logger.js";
-import { fetchMeta } from "@/misc/fetch-meta.js";
+import { fetchMeta } from "backend-rs";
 
 const accessDenied = {
 	message: "Access denied.",
@@ -117,7 +117,7 @@ export default async (
 	}
 
 	// private mode
-	const meta = await fetchMeta();
+	const meta = await fetchMeta(true);
 	if (
 		meta.privateMode &&
 		ep.meta.requireCredentialPrivateMode &&
diff --git a/packages/backend/src/server/api/endpoints/admin/meta.ts b/packages/backend/src/server/api/endpoints/admin/meta.ts
index a22fbab8f1..c7731c6c81 100644
--- a/packages/backend/src/server/api/endpoints/admin/meta.ts
+++ b/packages/backend/src/server/api/endpoints/admin/meta.ts
@@ -1,5 +1,5 @@
 import config from "@/config/index.js";
-import { fetchMeta } from "@/misc/fetch-meta.js";
+import { fetchMeta } from "backend-rs";
 import { MAX_NOTE_TEXT_LENGTH, MAX_CAPTION_TEXT_LENGTH } from "@/const.js";
 import define from "@/server/api/define.js";
 
@@ -466,7 +466,7 @@ export const paramDef = {
 } as const;
 
 export default define(meta, paramDef, async () => {
-	const instance = await fetchMeta(true);
+	const instance = await fetchMeta(false);
 
 	return {
 		maintainerName: instance.maintainerName,
diff --git a/packages/backend/src/server/api/endpoints/custom-motd.ts b/packages/backend/src/server/api/endpoints/custom-motd.ts
index 2939355b94..ac1012258d 100644
--- a/packages/backend/src/server/api/endpoints/custom-motd.ts
+++ b/packages/backend/src/server/api/endpoints/custom-motd.ts
@@ -1,5 +1,5 @@
 // import { IsNull } from 'typeorm';
-import { fetchMeta } from "@/misc/fetch-meta.js";
+import { fetchMeta } from "backend-rs";
 import define from "@/server/api/define.js";
 
 export const meta = {
@@ -27,7 +27,7 @@ export const paramDef = {
 } as const;
 
 export default define(meta, paramDef, async () => {
-	const meta = await fetchMeta();
+	const meta = await fetchMeta(true);
 	const motd = await Promise.all(meta.customMotd.map((x) => x));
 	return motd;
 });
diff --git a/packages/backend/src/server/api/endpoints/custom-splash-icons.ts b/packages/backend/src/server/api/endpoints/custom-splash-icons.ts
index f63a1b9600..4eb35aa3e5 100644
--- a/packages/backend/src/server/api/endpoints/custom-splash-icons.ts
+++ b/packages/backend/src/server/api/endpoints/custom-splash-icons.ts
@@ -1,5 +1,5 @@
 // import { IsNull } from 'typeorm';
-import { fetchMeta } from "@/misc/fetch-meta.js";
+import { fetchMeta } from "backend-rs";
 import define from "@/server/api/define.js";
 
 export const meta = {
@@ -27,7 +27,7 @@ export const paramDef = {
 } as const;
 
 export default define(meta, paramDef, async () => {
-	const meta = await fetchMeta();
+	const meta = await fetchMeta(true);
 	const icons = await Promise.all(meta.customSplashIcons.map((x) => x));
 	return icons;
 });
diff --git a/packages/backend/src/server/api/endpoints/drive.ts b/packages/backend/src/server/api/endpoints/drive.ts
index 164e7b8f93..c04f219a9b 100644
--- a/packages/backend/src/server/api/endpoints/drive.ts
+++ b/packages/backend/src/server/api/endpoints/drive.ts
@@ -1,4 +1,4 @@
-import { fetchMeta } from "@/misc/fetch-meta.js";
+import { fetchMeta } from "backend-rs";
 import { DriveFiles } from "@/models/index.js";
 import define from "@/server/api/define.js";
 
@@ -35,7 +35,7 @@ export const paramDef = {
 } as const;
 
 export default define(meta, paramDef, async (ps, user) => {
-	const instance = await fetchMeta(true);
+	const instance = await fetchMeta(false);
 
 	// Calculate drive usage
 	const usage = await DriveFiles.calcDriveUsageOf(user.id);
diff --git a/packages/backend/src/server/api/endpoints/drive/files/create.ts b/packages/backend/src/server/api/endpoints/drive/files/create.ts
index a3e3fafa2f..44e388a9bd 100644
--- a/packages/backend/src/server/api/endpoints/drive/files/create.ts
+++ b/packages/backend/src/server/api/endpoints/drive/files/create.ts
@@ -2,7 +2,7 @@ import { addFile } from "@/services/drive/add-file.js";
 import { DriveFiles } from "@/models/index.js";
 import { DB_MAX_IMAGE_COMMENT_LENGTH } from "@/misc/hard-limits.js";
 import { IdentifiableError } from "@/misc/identifiable-error.js";
-import { fetchMeta } from "@/misc/fetch-meta.js";
+import { fetchMeta } from "backend-rs";
 import { MINUTE } from "@/const.js";
 import define from "@/server/api/define.js";
 import { apiLogger } from "@/server/api/logger.js";
@@ -96,7 +96,7 @@ export default define(
 			name = null;
 		}
 
-		const instanceMeta = await fetchMeta();
+		const instanceMeta = await fetchMeta(true);
 
 		try {
 			// Create file
diff --git a/packages/backend/src/server/api/endpoints/federation/instances.ts b/packages/backend/src/server/api/endpoints/federation/instances.ts
index 27a6dabb49..8c021d0e65 100644
--- a/packages/backend/src/server/api/endpoints/federation/instances.ts
+++ b/packages/backend/src/server/api/endpoints/federation/instances.ts
@@ -1,6 +1,6 @@
 import define from "@/server/api/define.js";
 import { Instances } from "@/models/index.js";
-import { fetchMeta } from "@/misc/fetch-meta.js";
+import { fetchMeta } from "backend-rs";
 import { sqlLikeEscape } from "@/misc/sql-like-escape.js";
 
 export const meta = {
@@ -101,7 +101,7 @@ export default define(meta, paramDef, async (ps, me) => {
 	}
 
 	if (typeof ps.blocked === "boolean") {
-		const meta = await fetchMeta(true);
+		const meta = await fetchMeta(false);
 		if (ps.blocked) {
 			if (meta.blockedHosts.length === 0) {
 				return [];
@@ -117,7 +117,7 @@ export default define(meta, paramDef, async (ps, me) => {
 	}
 
 	if (typeof ps.silenced === "boolean") {
-		const meta = await fetchMeta(true);
+		const meta = await fetchMeta(false);
 		if (ps.silenced) {
 			if (meta.silencedHosts.length === 0) {
 				return [];
diff --git a/packages/backend/src/server/api/endpoints/hashtags/trend.ts b/packages/backend/src/server/api/endpoints/hashtags/trend.ts
index fe8bba95fd..9d31445a42 100644
--- a/packages/backend/src/server/api/endpoints/hashtags/trend.ts
+++ b/packages/backend/src/server/api/endpoints/hashtags/trend.ts
@@ -1,6 +1,6 @@
 import { Brackets } from "typeorm";
 import define from "@/server/api/define.js";
-import { fetchMeta } from "@/misc/fetch-meta.js";
+import { fetchMeta } from "backend-rs";
 import { Notes } from "@/models/index.js";
 import type { Note } from "@/models/entities/note.js";
 import { safeForSql } from "@/misc/safe-for-sql.js";
@@ -67,7 +67,7 @@ export const paramDef = {
 } as const;
 
 export default define(meta, paramDef, async () => {
-	const instance = await fetchMeta(true);
+	const instance = await fetchMeta(false);
 	const hiddenTags = instance.hiddenTags.map((t) => normalizeForSearch(t));
 
 	const now = new Date(); // 5分単位で丸めた現在日時
diff --git a/packages/backend/src/server/api/endpoints/i/import-posts.ts b/packages/backend/src/server/api/endpoints/i/import-posts.ts
index b8b52be98f..225306ebc5 100644
--- a/packages/backend/src/server/api/endpoints/i/import-posts.ts
+++ b/packages/backend/src/server/api/endpoints/i/import-posts.ts
@@ -3,7 +3,7 @@ import { createImportPostsJob } from "@/queue/index.js";
 import { ApiError } from "@/server/api/error.js";
 import { DriveFiles } from "@/models/index.js";
 import { DAY } from "@/const.js";
-import { fetchMeta } from "@/misc/fetch-meta.js";
+import { fetchMeta } from "backend-rs";
 
 export const meta = {
 	secure: true,
@@ -45,7 +45,7 @@ export const paramDef = {
 export default define(meta, paramDef, async (ps, user) => {
 	const file = await DriveFiles.findOneBy({ id: ps.fileId });
 
-	const instanceMeta = await fetchMeta();
+	const instanceMeta = await fetchMeta(true);
 	if (instanceMeta.experimentalFeatures?.postImports === false)
 		throw new ApiError(meta.errors.importsDisabled);
 
diff --git a/packages/backend/src/server/api/endpoints/meta.ts b/packages/backend/src/server/api/endpoints/meta.ts
index 2a674b52c3..31677ee2ef 100644
--- a/packages/backend/src/server/api/endpoints/meta.ts
+++ b/packages/backend/src/server/api/endpoints/meta.ts
@@ -1,7 +1,7 @@
 import JSON5 from "json5";
 import { IsNull, MoreThan } from "typeorm";
 import config from "@/config/index.js";
-import { fetchMeta } from "@/misc/fetch-meta.js";
+import { fetchMeta } from "backend-rs";
 import { Ads, Emojis, Users } from "@/models/index.js";
 import { MAX_NOTE_TEXT_LENGTH, MAX_CAPTION_TEXT_LENGTH } from "@/const.js";
 import define from "@/server/api/define.js";
@@ -398,7 +398,7 @@ export const paramDef = {
 } as const;
 
 export default define(meta, paramDef, async (ps, me) => {
-	const instance = await fetchMeta(true);
+	const instance = await fetchMeta(false);
 
 	const emojis = await Emojis.find({
 		where: {
diff --git a/packages/backend/src/server/api/endpoints/notes/global-timeline.ts b/packages/backend/src/server/api/endpoints/notes/global-timeline.ts
index 142b380f71..476375dc0b 100644
--- a/packages/backend/src/server/api/endpoints/notes/global-timeline.ts
+++ b/packages/backend/src/server/api/endpoints/notes/global-timeline.ts
@@ -1,4 +1,4 @@
-import { fetchMeta } from "@/misc/fetch-meta.js";
+import { fetchMeta } from "backend-rs";
 import { Notes } from "@/models/index.js";
 import { activeUsersChart } from "@/services/chart/index.js";
 import define from "@/server/api/define.js";
@@ -64,7 +64,7 @@ export const paramDef = {
 } as const;
 
 export default define(meta, paramDef, async (ps, user) => {
-	const m = await fetchMeta();
+	const m = await fetchMeta(true);
 	if (m.disableGlobalTimeline) {
 		if (user == null || !(user.isAdmin || user.isModerator)) {
 			throw new ApiError(meta.errors.gtlDisabled);
diff --git a/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts b/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts
index c9800f2e1f..e6ab910040 100644
--- a/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts
+++ b/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts
@@ -1,5 +1,5 @@
 import { Brackets } from "typeorm";
-import { fetchMeta } from "@/misc/fetch-meta.js";
+import { fetchMeta } from "backend-rs";
 import { Followings, Notes } from "@/models/index.js";
 import { activeUsersChart } from "@/services/chart/index.js";
 import define from "@/server/api/define.js";
@@ -71,7 +71,7 @@ export const paramDef = {
 } as const;
 
 export default define(meta, paramDef, async (ps, user) => {
-	const m = await fetchMeta();
+	const m = await fetchMeta(true);
 	if (m.disableLocalTimeline && !user.isAdmin && !user.isModerator) {
 		throw new ApiError(meta.errors.stlDisabled);
 	}
diff --git a/packages/backend/src/server/api/endpoints/notes/local-timeline.ts b/packages/backend/src/server/api/endpoints/notes/local-timeline.ts
index b9cb68c2a0..2a99c1236c 100644
--- a/packages/backend/src/server/api/endpoints/notes/local-timeline.ts
+++ b/packages/backend/src/server/api/endpoints/notes/local-timeline.ts
@@ -1,5 +1,5 @@
 import { Brackets } from "typeorm";
-import { fetchMeta } from "@/misc/fetch-meta.js";
+import { fetchMeta } from "backend-rs";
 import { Notes } from "@/models/index.js";
 import { activeUsersChart } from "@/services/chart/index.js";
 import define from "@/server/api/define.js";
@@ -74,7 +74,7 @@ export const paramDef = {
 } as const;
 
 export default define(meta, paramDef, async (ps, user) => {
-	const m = await fetchMeta();
+	const m = await fetchMeta(true);
 	if (m.disableLocalTimeline) {
 		if (user == null || !(user.isAdmin || user.isModerator)) {
 			throw new ApiError(meta.errors.ltlDisabled);
diff --git a/packages/backend/src/server/api/endpoints/notes/recommended-timeline.ts b/packages/backend/src/server/api/endpoints/notes/recommended-timeline.ts
index f71822f926..073a8f8569 100644
--- a/packages/backend/src/server/api/endpoints/notes/recommended-timeline.ts
+++ b/packages/backend/src/server/api/endpoints/notes/recommended-timeline.ts
@@ -1,5 +1,5 @@
 import { Brackets } from "typeorm";
-import { fetchMeta } from "@/misc/fetch-meta.js";
+import { fetchMeta } from "backend-rs";
 import { Notes } from "@/models/index.js";
 import { activeUsersChart } from "@/services/chart/index.js";
 import define from "@/server/api/define.js";
@@ -74,7 +74,7 @@ export const paramDef = {
 } as const;
 
 export default define(meta, paramDef, async (ps, user) => {
-	const m = await fetchMeta();
+	const m = await fetchMeta(true);
 	if (m.disableRecommendedTimeline) {
 		if (user == null || !(user.isAdmin || user.isModerator)) {
 			throw new ApiError(meta.errors.rtlDisabled);
diff --git a/packages/backend/src/server/api/endpoints/pinned-users.ts b/packages/backend/src/server/api/endpoints/pinned-users.ts
index 6d6519e47b..325b54f350 100644
--- a/packages/backend/src/server/api/endpoints/pinned-users.ts
+++ b/packages/backend/src/server/api/endpoints/pinned-users.ts
@@ -1,6 +1,6 @@
 import { IsNull } from "typeorm";
 import { Users } from "@/models/index.js";
-import { fetchMeta } from "@/misc/fetch-meta.js";
+import { fetchMeta } from "backend-rs";
 import { stringToAcct } from "backend-rs";
 import type { User } from "@/models/entities/user.js";
 import define from "@/server/api/define.js";
@@ -31,7 +31,7 @@ export const paramDef = {
 } as const;
 
 export default define(meta, paramDef, async (ps, me) => {
-	const meta = await fetchMeta();
+	const meta = await fetchMeta(true);
 
 	const users = await Promise.all(
 		meta.pinnedUsers
diff --git a/packages/backend/src/server/api/endpoints/recommended-instances.ts b/packages/backend/src/server/api/endpoints/recommended-instances.ts
index b235678428..5c5e267b2e 100644
--- a/packages/backend/src/server/api/endpoints/recommended-instances.ts
+++ b/packages/backend/src/server/api/endpoints/recommended-instances.ts
@@ -1,5 +1,5 @@
 // import { IsNull } from 'typeorm';
-import { fetchMeta } from "@/misc/fetch-meta.js";
+import { fetchMeta } from "backend-rs";
 import define from "@/server/api/define.js";
 
 export const meta = {
@@ -27,7 +27,7 @@ export const paramDef = {
 } as const;
 
 export default define(meta, paramDef, async () => {
-	const meta = await fetchMeta();
+	const meta = await fetchMeta(true);
 	const instances = await Promise.all(meta.recommendedInstances.map((x) => x));
 	return instances;
 });
diff --git a/packages/backend/src/server/api/endpoints/server-info.ts b/packages/backend/src/server/api/endpoints/server-info.ts
index d3b6a08074..1a1ecad688 100644
--- a/packages/backend/src/server/api/endpoints/server-info.ts
+++ b/packages/backend/src/server/api/endpoints/server-info.ts
@@ -1,7 +1,7 @@
 import * as os from "node:os";
 import si from "systeminformation";
 import define from "@/server/api/define.js";
-import { fetchMeta } from "@/misc/fetch-meta.js";
+import { fetchMeta } from "backend-rs";
 
 export const meta = {
 	requireCredential: false,
@@ -30,7 +30,7 @@ export default define(meta, paramDef, async () => {
 		}
 	}
 
-	const instanceMeta = await fetchMeta();
+	const instanceMeta = await fetchMeta(true);
 	if (!instanceMeta.enableServerMachineStats) {
 		return {
 			machine: "Not specified",
diff --git a/packages/backend/src/server/api/endpoints/sw/register.ts b/packages/backend/src/server/api/endpoints/sw/register.ts
index 528d3106e0..69b3f6779b 100644
--- a/packages/backend/src/server/api/endpoints/sw/register.ts
+++ b/packages/backend/src/server/api/endpoints/sw/register.ts
@@ -1,4 +1,4 @@
-import { fetchMeta } from "@/misc/fetch-meta.js";
+import { fetchMeta } from "backend-rs";
 import { genId } from "backend-rs";
 import { SwSubscriptions } from "@/models/index.js";
 import define from "@/server/api/define.js";
@@ -64,7 +64,7 @@ export default define(meta, paramDef, async (ps, me) => {
 		publickey: ps.publickey,
 	});
 
-	const instance = await fetchMeta(true);
+	const instance = await fetchMeta(false);
 
 	// if already subscribed
 	if (subscription != null) {
diff --git a/packages/backend/src/server/api/endpoints/users/report-abuse.ts b/packages/backend/src/server/api/endpoints/users/report-abuse.ts
index 1b43762cb6..fda4aa0bb8 100644
--- a/packages/backend/src/server/api/endpoints/users/report-abuse.ts
+++ b/packages/backend/src/server/api/endpoints/users/report-abuse.ts
@@ -4,7 +4,7 @@ import { publishAdminStream } from "@/services/stream.js";
 import { AbuseUserReports, UserProfiles, Users } from "@/models/index.js";
 import { genId } from "backend-rs";
 import { sendEmail } from "@/services/send-email.js";
-import { fetchMeta } from "@/misc/fetch-meta.js";
+import { fetchMeta } from "backend-rs";
 import { getUser } from "@/server/api/common/getters.js";
 import { ApiError } from "@/server/api/error.js";
 import define from "@/server/api/define.js";
@@ -86,7 +86,7 @@ export default define(meta, paramDef, async (ps, me) => {
 			],
 		});
 
-		const meta = await fetchMeta();
+		const meta = await fetchMeta(true);
 		for (const moderator of moderators) {
 			publishAdminStream(moderator.id, "newAbuseUserReport", {
 				id: report.id,
diff --git a/packages/backend/src/server/api/mastodon/endpoints/meta.ts b/packages/backend/src/server/api/mastodon/endpoints/meta.ts
index fbad7d5ef4..5c304929a1 100644
--- a/packages/backend/src/server/api/mastodon/endpoints/meta.ts
+++ b/packages/backend/src/server/api/mastodon/endpoints/meta.ts
@@ -1,6 +1,6 @@
 import { Entity } from "megalodon";
 import config from "@/config/index.js";
-import { fetchMeta } from "@/misc/fetch-meta.js";
+import { fetchMeta } from "backend-rs";
 import { Users, Notes } from "@/models/index.js";
 import { IsNull } from "typeorm";
 import { MAX_NOTE_TEXT_LENGTH, FILE_TYPE_BROWSERSAFE } from "@/const.js";
@@ -10,7 +10,7 @@ export async function getInstance(
 	contact: Entity.Account,
 ) {
 	const [meta, totalUsers, totalStatuses] = await Promise.all([
-		fetchMeta(),
+		fetchMeta(true),
 		Users.count({ where: { host: IsNull() } }),
 		Notes.count({ where: { userHost: IsNull() } }),
 	]);
diff --git a/packages/backend/src/server/api/mastodon/endpoints/status.ts b/packages/backend/src/server/api/mastodon/endpoints/status.ts
index 5e6c0edaae..5286c90fac 100644
--- a/packages/backend/src/server/api/mastodon/endpoints/status.ts
+++ b/packages/backend/src/server/api/mastodon/endpoints/status.ts
@@ -4,14 +4,13 @@ import { emojiRegexAtStartToEnd } from "@/misc/emoji-regex.js";
 import querystring from "node:querystring";
 import qs from "qs";
 import { convertTimelinesArgsId, limitToInt } from "./timeline.js";
-import { fromMastodonId } from "backend-rs";
+import { fetchMeta, fromMastodonId } from "backend-rs";
 import {
 	convertAccount,
 	convertAttachment,
 	convertPoll,
 	convertStatus,
 } from "../converters.js";
-import { fetchMeta } from "@/misc/fetch-meta.js";
 import { apiLogger } from "@/server/api/logger.js";
 import { inspect } from "node:util";
 
@@ -213,7 +212,7 @@ export function apiStatusMastodon(router: Router): void {
 	router.post<{ Params: { id: string } }>(
 		"/v1/statuses/:id/favourite",
 		async (ctx) => {
-			const meta = await fetchMeta();
+			const meta = await fetchMeta(true);
 			const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
 			const accessTokens = ctx.headers.authorization;
 			const client = getClient(BASE_URL, accessTokens);
@@ -235,7 +234,7 @@ export function apiStatusMastodon(router: Router): void {
 	router.post<{ Params: { id: string } }>(
 		"/v1/statuses/:id/unfavourite",
 		async (ctx) => {
-			const meta = await fetchMeta();
+			const meta = await fetchMeta(true);
 			const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
 			const accessTokens = ctx.headers.authorization;
 			const client = getClient(BASE_URL, accessTokens);
diff --git a/packages/backend/src/server/api/private/signup.ts b/packages/backend/src/server/api/private/signup.ts
index 64b5c57337..5af5d65b50 100644
--- a/packages/backend/src/server/api/private/signup.ts
+++ b/packages/backend/src/server/api/private/signup.ts
@@ -1,18 +1,17 @@
 import type Koa from "koa";
 import rndstr from "rndstr";
-import { fetchMeta } from "@/misc/fetch-meta.js";
 import { verifyHcaptcha, verifyRecaptcha } from "@/misc/captcha.js";
 import { Users, RegistrationTickets, UserPendings } from "@/models/index.js";
 import { signup } from "@/server/api/common/signup.js";
 import config from "@/config/index.js";
 import { sendEmail } from "@/services/send-email.js";
-import { genId, hashPassword } from "backend-rs";
+import { fetchMeta, genId, hashPassword } from "backend-rs";
 import { validateEmailForAccount } from "@/services/validate-email-for-account.js";
 
 export default async (ctx: Koa.Context) => {
 	const body = ctx.request.body;
 
-	const instance = await fetchMeta(true);
+	const instance = await fetchMeta(false);
 
 	// Verify *Captcha
 	// ただしテスト時はこの機構は障害となるため無効にする
diff --git a/packages/backend/src/server/api/stream/channels/global-timeline.ts b/packages/backend/src/server/api/stream/channels/global-timeline.ts
index 97295af57a..1760d5abf7 100644
--- a/packages/backend/src/server/api/stream/channels/global-timeline.ts
+++ b/packages/backend/src/server/api/stream/channels/global-timeline.ts
@@ -1,6 +1,5 @@
 import Channel from "../channel.js";
-import { fetchMeta } from "@/misc/fetch-meta.js";
-import { checkWordMute } from "backend-rs";
+import { checkWordMute, fetchMeta } from "backend-rs";
 import { isInstanceMuted } from "@/misc/is-instance-muted.js";
 import { isUserRelated } from "@/misc/is-user-related.js";
 import type { Packed } from "@/misc/schema.js";
@@ -17,7 +16,7 @@ export default class extends Channel {
 	}
 
 	public async init(params: any) {
-		const meta = await fetchMeta();
+		const meta = await fetchMeta(true);
 		if (meta.disableGlobalTimeline) {
 			if (this.user == null || !(this.user.isAdmin || this.user.isModerator))
 				return;
diff --git a/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts b/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts
index 9052e7c2a5..5100a48efd 100644
--- a/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts
+++ b/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts
@@ -1,6 +1,5 @@
 import Channel from "../channel.js";
-import { fetchMeta } from "@/misc/fetch-meta.js";
-import { checkWordMute } from "backend-rs";
+import { checkWordMute, fetchMeta } from "backend-rs";
 import { isUserRelated } from "@/misc/is-user-related.js";
 import { isInstanceMuted } from "@/misc/is-instance-muted.js";
 import type { Packed } from "@/misc/schema.js";
@@ -17,7 +16,7 @@ export default class extends Channel {
 	}
 
 	public async init(params: any) {
-		const meta = await fetchMeta();
+		const meta = await fetchMeta(true);
 		if (
 			meta.disableLocalTimeline &&
 			!this.user!.isAdmin &&
diff --git a/packages/backend/src/server/api/stream/channels/local-timeline.ts b/packages/backend/src/server/api/stream/channels/local-timeline.ts
index bd31c94f9d..2c9a38d677 100644
--- a/packages/backend/src/server/api/stream/channels/local-timeline.ts
+++ b/packages/backend/src/server/api/stream/channels/local-timeline.ts
@@ -1,6 +1,5 @@
 import Channel from "../channel.js";
-import { fetchMeta } from "@/misc/fetch-meta.js";
-import { checkWordMute } from "backend-rs";
+import { checkWordMute, fetchMeta } from "backend-rs";
 import { isUserRelated } from "@/misc/is-user-related.js";
 import type { Packed } from "@/misc/schema.js";
 
@@ -16,7 +15,7 @@ export default class extends Channel {
 	}
 
 	public async init(params: any) {
-		const meta = await fetchMeta();
+		const meta = await fetchMeta(true);
 		if (meta.disableLocalTimeline) {
 			if (this.user == null || !(this.user.isAdmin || this.user.isModerator))
 				return;
diff --git a/packages/backend/src/server/api/stream/channels/recommended-timeline.ts b/packages/backend/src/server/api/stream/channels/recommended-timeline.ts
index 26c3cbfc68..5d0d6fc602 100644
--- a/packages/backend/src/server/api/stream/channels/recommended-timeline.ts
+++ b/packages/backend/src/server/api/stream/channels/recommended-timeline.ts
@@ -1,6 +1,5 @@
 import Channel from "../channel.js";
-import { fetchMeta } from "@/misc/fetch-meta.js";
-import { checkWordMute } from "backend-rs";
+import { checkWordMute, fetchMeta } from "backend-rs";
 import { isUserRelated } from "@/misc/is-user-related.js";
 import { isInstanceMuted } from "@/misc/is-instance-muted.js";
 import type { Packed } from "@/misc/schema.js";
@@ -17,7 +16,7 @@ export default class extends Channel {
 	}
 
 	public async init(params: any) {
-		const meta = await fetchMeta();
+		const meta = await fetchMeta(true);
 		if (
 			meta.disableRecommendedTimeline &&
 			!this.user!.isAdmin &&
@@ -37,7 +36,7 @@ export default class extends Channel {
 		// チャンネルの投稿ではなく、その投稿のユーザーをフォローしている または
 		// チャンネルの投稿ではなく、全体公開のローカルの投稿 または
 		// フォローしているチャンネルの投稿 の場合だけ
-		const meta = await fetchMeta();
+		const meta = await fetchMeta(true);
 		if (
 			!(
 				note.user.host != null &&
diff --git a/packages/backend/src/server/index.ts b/packages/backend/src/server/index.ts
index 2a6dfdf674..6cf837b4ed 100644
--- a/packages/backend/src/server/index.ts
+++ b/packages/backend/src/server/index.ts
@@ -16,7 +16,7 @@ import { IsNull } from "typeorm";
 import config from "@/config/index.js";
 import Logger from "@/services/logger.js";
 import { Users } from "@/models/index.js";
-import { fetchMeta } from "@/misc/fetch-meta.js";
+import { fetchMeta } from "backend-rs";
 import { genIdenticon } from "@/misc/gen-identicon.js";
 import { createTemp } from "@/misc/create-temp.js";
 import { stringToAcct } from "backend-rs";
@@ -126,7 +126,7 @@ router.get("/avatar/@:acct", async (ctx) => {
 });
 
 router.get("/identicon/:x", async (ctx) => {
-	const meta = await fetchMeta();
+	const meta = await fetchMeta(true);
 	if (meta.enableIdenticonGeneration) {
 		const [temp, cleanup] = await createTemp();
 		await genIdenticon(ctx.params.x, fs.createWriteStream(temp));
diff --git a/packages/backend/src/server/nodeinfo.ts b/packages/backend/src/server/nodeinfo.ts
index 1cb8eb1eaf..7359878b19 100644
--- a/packages/backend/src/server/nodeinfo.ts
+++ b/packages/backend/src/server/nodeinfo.ts
@@ -1,6 +1,6 @@
 import Router from "@koa/router";
 import config from "@/config/index.js";
-import { fetchMeta } from "@/misc/fetch-meta.js";
+import { fetchMeta } from "backend-rs";
 import { Users, Notes } from "@/models/index.js";
 import { IsNull, MoreThan } from "typeorm";
 import { MAX_NOTE_TEXT_LENGTH, MAX_CAPTION_TEXT_LENGTH } from "@/const.js";
@@ -27,7 +27,7 @@ const nodeinfo2 = async () => {
 	const now = Date.now();
 	const [meta, total, activeHalfyear, activeMonth, localPosts] =
 		await Promise.all([
-			fetchMeta(true),
+			fetchMeta(false),
 			Users.count({ where: { host: IsNull() } }),
 			Users.count({
 				where: {
diff --git a/packages/backend/src/server/web/index.ts b/packages/backend/src/server/web/index.ts
index bb17cd279a..6473073370 100644
--- a/packages/backend/src/server/web/index.ts
+++ b/packages/backend/src/server/web/index.ts
@@ -10,13 +10,12 @@ import Router from "@koa/router";
 import send from "koa-send";
 import favicon from "koa-favicon";
 import views from "@ladjs/koa-views";
-import sharp from "sharp";
 import { createBullBoard } from "@bull-board/api";
 import { BullAdapter } from "@bull-board/api/bullAdapter.js";
 import { KoaAdapter } from "@bull-board/koa";
 
 import { In, IsNull } from "typeorm";
-import { fetchMeta, metaToPugArgs } from "@/misc/fetch-meta.js";
+import { fetchMeta, metaToPugArgs } from "backend-rs";
 import config from "@/config/index.js";
 import {
 	Users,
@@ -326,7 +325,7 @@ const getFeed = async (
 	noRenotes: string,
 	noReplies: string,
 ) => {
-	const meta = await fetchMeta();
+	const meta = await fetchMeta(true);
 	if (meta.privateMode) {
 		return;
 	}
@@ -475,7 +474,7 @@ const userPage: Router.Middleware = async (ctx, next) => {
 	}
 
 	const profile = await UserProfiles.findOneByOrFail({ userId: user.id });
-	const meta = await fetchMeta();
+	const meta = await fetchMeta(true);
 	const me = profile.fields
 		? profile.fields
 				.filter((filed) => filed.value?.match(/^https?:/))
@@ -524,7 +523,7 @@ router.get("/notes/:note", async (ctx, next) => {
 			const profile = await UserProfiles.findOneByOrFail({
 				userId: note.userId,
 			});
-			const meta = await fetchMeta();
+			const meta = await fetchMeta(true);
 			await ctx.render("note", {
 				...metaToPugArgs(meta),
 				note: _note,
@@ -558,7 +557,7 @@ router.get("/posts/:note", async (ctx, next) => {
 	if (note) {
 		const _note = await Notes.pack(note);
 		const profile = await UserProfiles.findOneByOrFail({ userId: note.userId });
-		const meta = await fetchMeta();
+		const meta = await fetchMeta(true);
 		await ctx.render("note", {
 			...metaToPugArgs(meta),
 			note: _note,
@@ -596,7 +595,7 @@ router.get("/@:user/pages/:page", async (ctx, next) => {
 	if (page) {
 		const _page = await Pages.pack(page);
 		const profile = await UserProfiles.findOneByOrFail({ userId: page.userId });
-		const meta = await fetchMeta();
+		const meta = await fetchMeta(true);
 		await ctx.render("page", {
 			...metaToPugArgs(meta),
 			page: _page,
@@ -628,7 +627,7 @@ router.get("/clips/:clip", async (ctx, next) => {
 	if (clip) {
 		const _clip = await Clips.pack(clip);
 		const profile = await UserProfiles.findOneByOrFail({ userId: clip.userId });
-		const meta = await fetchMeta();
+		const meta = await fetchMeta(true);
 		await ctx.render("clip", {
 			...metaToPugArgs(meta),
 			clip: _clip,
@@ -653,7 +652,7 @@ router.get("/gallery/:post", async (ctx, next) => {
 	if (post) {
 		const _post = await GalleryPosts.pack(post);
 		const profile = await UserProfiles.findOneByOrFail({ userId: post.userId });
-		const meta = await fetchMeta();
+		const meta = await fetchMeta(true);
 		await ctx.render("gallery-post", {
 			...metaToPugArgs(meta),
 			post: _post,
@@ -679,7 +678,7 @@ router.get("/channels/:channel", async (ctx, next) => {
 
 	if (channel) {
 		const _channel = await Channels.pack(channel);
-		const meta = await fetchMeta();
+		const meta = await fetchMeta(true);
 		await ctx.render("channel", {
 			...metaToPugArgs(meta),
 			channel: _channel,
@@ -732,7 +731,7 @@ router.get("/api/v1/streaming", async (ctx) => {
 
 // Render base html for all requests
 router.get("(.*)", async (ctx) => {
-	const meta = await fetchMeta();
+	const meta = await fetchMeta(true);
 
 	await ctx.render("base", {
 		...metaToPugArgs(meta),
diff --git a/packages/backend/src/server/web/manifest.ts b/packages/backend/src/server/web/manifest.ts
index bbcf639ffe..a4c615c7ab 100644
--- a/packages/backend/src/server/web/manifest.ts
+++ b/packages/backend/src/server/web/manifest.ts
@@ -1,5 +1,5 @@
 import type Koa from "koa";
-import { fetchMeta } from "@/misc/fetch-meta.js";
+import { fetchMeta } from "backend-rs";
 import config from "@/config/index.js";
 import manifest from "./manifest.json" assert { type: "json" };
 
@@ -8,7 +8,7 @@ export const manifestHandler = async (ctx: Koa.Context) => {
 	//const res = structuredClone(manifest);
 	const res = JSON.parse(JSON.stringify(manifest));
 
-	const instance = await fetchMeta(true);
+	const instance = await fetchMeta(false);
 
 	res.short_name = instance.name || "Firefish";
 	res.name = instance.name || "Firefish";
diff --git a/packages/backend/src/server/web/url-preview.ts b/packages/backend/src/server/web/url-preview.ts
index 07d3bf7f2c..f59f3f357a 100644
--- a/packages/backend/src/server/web/url-preview.ts
+++ b/packages/backend/src/server/web/url-preview.ts
@@ -1,6 +1,6 @@
 import type Koa from "koa";
 import summaly from "summaly";
-import { fetchMeta } from "@/misc/fetch-meta.js";
+import { fetchMeta } from "backend-rs";
 import Logger from "@/services/logger.js";
 import config from "@/config/index.js";
 import { query } from "@/prelude/url.js";
@@ -22,7 +22,7 @@ export const urlPreviewHandler = async (ctx: Koa.Context) => {
 		return;
 	}
 
-	const meta = await fetchMeta();
+	const meta = await fetchMeta(true);
 
 	logger.info(
 		meta.summalyProxy
diff --git a/packages/backend/src/services/drive/add-file.ts b/packages/backend/src/services/drive/add-file.ts
index 6320277eef..24ad9f8f02 100644
--- a/packages/backend/src/services/drive/add-file.ts
+++ b/packages/backend/src/services/drive/add-file.ts
@@ -6,7 +6,7 @@ import type S3 from "aws-sdk/clients/s3.js"; // TODO: migrate to SDK v3
 import sharp from "sharp";
 import { IsNull } from "typeorm";
 import { publishMainStream, publishDriveStream } from "@/services/stream.js";
-import { fetchMeta } from "@/misc/fetch-meta.js";
+import { fetchMeta } from "backend-rs";
 import { contentDisposition } from "@/misc/content-disposition.js";
 import { getFileInfo } from "@/misc/get-file-info.js";
 import {
@@ -77,7 +77,7 @@ async function save(
 	// thunbnail, webpublic を必要なら生成
 	const alts = await generateAlts(path, type, !file.uri);
 
-	const meta = await fetchMeta();
+	const meta = await fetchMeta(true);
 
 	if (meta.useObjectStorage) {
 		//#region ObjectStorage params
@@ -360,7 +360,7 @@ async function upload(
 	if (type === "image/apng") type = "image/png";
 	if (!FILE_TYPE_BROWSERSAFE.includes(type)) type = "application/octet-stream";
 
-	const meta = await fetchMeta();
+	const meta = await fetchMeta(true);
 
 	const params = {
 		Bucket: meta.objectStorageBucket,
@@ -495,7 +495,7 @@ export async function addFile({
 		const usage = await DriveFiles.calcDriveUsageOf(user);
 		const u = await Users.findOneBy({ id: user.id });
 
-		const instance = await fetchMeta();
+		const instance = await fetchMeta(true);
 		let driveCapacity =
 			1024 *
 			1024 *
@@ -567,7 +567,7 @@ export async function addFile({
 		: null;
 
 	const folder = await fetchFolder();
-	const instance = await fetchMeta();
+	const instance = await fetchMeta(true);
 
 	let file = new DriveFile();
 	file.id = genId();
diff --git a/packages/backend/src/services/drive/delete-file.ts b/packages/backend/src/services/drive/delete-file.ts
index 16c0219e71..b4b5580a1c 100644
--- a/packages/backend/src/services/drive/delete-file.ts
+++ b/packages/backend/src/services/drive/delete-file.ts
@@ -2,7 +2,7 @@ import type { DriveFile } from "@/models/entities/drive-file.js";
 import { InternalStorage } from "./internal-storage.js";
 import { DriveFiles } from "@/models/index.js";
 import { createDeleteObjectStorageFileJob } from "@/queue/index.js";
-import { fetchMeta } from "@/misc/fetch-meta.js";
+import { fetchMeta } from "backend-rs";
 import { getS3 } from "./s3.js";
 import { v4 as uuid } from "uuid";
 
@@ -82,7 +82,7 @@ async function postProcess(file: DriveFile, isExpired = false) {
 }
 
 export async function deleteObjectStorageFile(key: string) {
-	const meta = await fetchMeta();
+	const meta = await fetchMeta(true);
 
 	const s3 = getS3(meta);
 
diff --git a/packages/backend/src/services/push-notification.ts b/packages/backend/src/services/push-notification.ts
index 09749059a9..1a772ff9c5 100644
--- a/packages/backend/src/services/push-notification.ts
+++ b/packages/backend/src/services/push-notification.ts
@@ -1,7 +1,7 @@
 import push from "web-push";
 import config from "@/config/index.js";
 import { SwSubscriptions } from "@/models/index.js";
-import { fetchMeta } from "@/misc/fetch-meta.js";
+import { fetchMeta } from "backend-rs";
 import type { Packed } from "@/misc/schema.js";
 import { getNoteSummary } from "@/misc/get-note-summary.js";
 
@@ -45,7 +45,7 @@ export async function pushNotification<T extends keyof pushNotificationsTypes>(
 	type: T,
 	body: pushNotificationsTypes[T],
 ) {
-	const meta = await fetchMeta();
+	const meta = await fetchMeta(true);
 
 	if (
 		!meta.enableServiceWorker ||
diff --git a/packages/backend/src/services/send-email.ts b/packages/backend/src/services/send-email.ts
index aa96cfc014..11a899d267 100644
--- a/packages/backend/src/services/send-email.ts
+++ b/packages/backend/src/services/send-email.ts
@@ -1,5 +1,5 @@
 import * as nodemailer from "nodemailer";
-import { fetchMeta } from "@/misc/fetch-meta.js";
+import { fetchMeta } from "backend-rs";
 import Logger from "@/services/logger.js";
 import config from "@/config/index.js";
 import { inspect } from "node:util";
@@ -12,7 +12,7 @@ export async function sendEmail(
 	html: string,
 	text: string,
 ) {
-	const meta = await fetchMeta(true);
+	const meta = await fetchMeta(false);
 
 	const iconUrl = `${config.url}/static-assets/mi-white.png`;
 	const emailSettingUrl = `${config.url}/settings/email`;
diff --git a/packages/backend/src/services/validate-email-for-account.ts b/packages/backend/src/services/validate-email-for-account.ts
index 4d05afcc6d..5aa091a5ac 100644
--- a/packages/backend/src/services/validate-email-for-account.ts
+++ b/packages/backend/src/services/validate-email-for-account.ts
@@ -1,12 +1,12 @@
 import { validate as validateEmail } from "deep-email-validator";
 import { UserProfiles } from "@/models/index.js";
-import { fetchMeta } from "@/misc/fetch-meta.js";
+import { fetchMeta } from "backend-rs";
 
 export async function validateEmailForAccount(emailAddress: string): Promise<{
 	available: boolean;
 	reason: null | "used" | "format" | "disposable" | "mx" | "smtp";
 }> {
-	const meta = await fetchMeta();
+	const meta = await fetchMeta(true);
 
 	const exist = await UserProfiles.countBy({
 		emailVerified: true,

From fca48b2a816fcef6ad17ccb0c31a2c9a15c31e54 Mon Sep 17 00:00:00 2001
From: naskya <m@naskya.net>
Date: Sun, 14 Apr 2024 20:29:44 +0900
Subject: [PATCH 058/110] refactor (backend): port safe-for-sql,
 sql-like-escape to backend-rs

---
 packages/backend-rs/index.d.ts                |  2 ++
 packages/backend-rs/index.js                  |  4 ++-
 packages/backend-rs/src/misc/escape_sql.rs    | 36 +++++++++++++++++++
 packages/backend-rs/src/misc/mod.rs           |  1 +
 packages/backend/src/misc/safe-for-sql.ts     |  3 --
 packages/backend/src/misc/sql-like-escape.ts  |  3 --
 .../api/endpoints/admin/emoji/list-remote.ts  |  3 +-
 .../server/api/endpoints/admin/emoji/list.ts  |  4 +--
 .../server/api/endpoints/admin/show-users.ts  |  2 +-
 .../server/api/endpoints/channels/search.ts   |  2 +-
 .../api/endpoints/federation/instances.ts     |  3 +-
 .../server/api/endpoints/hashtags/search.ts   |  2 +-
 .../server/api/endpoints/hashtags/trend.ts    |  3 +-
 .../api/endpoints/notes/search-by-tag.ts      |  2 +-
 .../src/server/api/endpoints/notes/search.ts  |  4 +--
 .../users/search-by-username-and-host.ts      |  2 +-
 .../src/server/api/endpoints/users/search.ts  |  2 +-
 17 files changed, 55 insertions(+), 23 deletions(-)
 create mode 100644 packages/backend-rs/src/misc/escape_sql.rs
 delete mode 100644 packages/backend/src/misc/safe-for-sql.ts
 delete mode 100644 packages/backend/src/misc/sql-like-escape.ts

diff --git a/packages/backend-rs/index.d.ts b/packages/backend-rs/index.d.ts
index 3770056d8f..a9398aacc1 100644
--- a/packages/backend-rs/index.d.ts
+++ b/packages/backend-rs/index.d.ts
@@ -132,6 +132,8 @@ export function isSelfHost(host?: string | undefined | null): boolean
 export function isSameOrigin(uri: string): boolean
 export function extractHost(uri: string): string
 export function toPuny(host: string): string
+export function sqlLikeEscape(src: string): string
+export function safeForSql(src: string): boolean
 /** Convert milliseconds to a human readable string */
 export function formatMilliseconds(milliseconds: number): string
 export function toMastodonId(firefishId: string): string | null
diff --git a/packages/backend-rs/index.js b/packages/backend-rs/index.js
index 363c858e4a..7a404d6447 100644
--- a/packages/backend-rs/index.js
+++ b/packages/backend-rs/index.js
@@ -310,7 +310,7 @@ if (!nativeBinding) {
   throw new Error(`Failed to load native binding`)
 }
 
-const { readServerConfig, stringToAcct, acctToString, checkWordMute, getFullApAccount, isSelfHost, isSameOrigin, extractHost, toPuny, formatMilliseconds, toMastodonId, fromMastodonId, fetchMeta, metaToPugArgs, nyaify, hashPassword, verifyPassword, isOldPasswordAlgorithm, AntennaSrcEnum, MutedNoteReasonEnum, NoteVisibilityEnum, NotificationTypeEnum, PageVisibilityEnum, PollNotevisibilityEnum, RelayStatusEnum, UserEmojimodpermEnum, UserProfileFfvisibilityEnum, UserProfileMutingnotificationtypesEnum, initIdGenerator, getTimestamp, genId, secureRndstr } = nativeBinding
+const { readServerConfig, stringToAcct, acctToString, checkWordMute, getFullApAccount, isSelfHost, isSameOrigin, extractHost, toPuny, sqlLikeEscape, safeForSql, formatMilliseconds, toMastodonId, fromMastodonId, fetchMeta, metaToPugArgs, nyaify, hashPassword, verifyPassword, isOldPasswordAlgorithm, AntennaSrcEnum, MutedNoteReasonEnum, NoteVisibilityEnum, NotificationTypeEnum, PageVisibilityEnum, PollNotevisibilityEnum, RelayStatusEnum, UserEmojimodpermEnum, UserProfileFfvisibilityEnum, UserProfileMutingnotificationtypesEnum, initIdGenerator, getTimestamp, genId, secureRndstr } = nativeBinding
 
 module.exports.readServerConfig = readServerConfig
 module.exports.stringToAcct = stringToAcct
@@ -321,6 +321,8 @@ module.exports.isSelfHost = isSelfHost
 module.exports.isSameOrigin = isSameOrigin
 module.exports.extractHost = extractHost
 module.exports.toPuny = toPuny
+module.exports.sqlLikeEscape = sqlLikeEscape
+module.exports.safeForSql = safeForSql
 module.exports.formatMilliseconds = formatMilliseconds
 module.exports.toMastodonId = toMastodonId
 module.exports.fromMastodonId = fromMastodonId
diff --git a/packages/backend-rs/src/misc/escape_sql.rs b/packages/backend-rs/src/misc/escape_sql.rs
new file mode 100644
index 0000000000..c575e088ce
--- /dev/null
+++ b/packages/backend-rs/src/misc/escape_sql.rs
@@ -0,0 +1,36 @@
+#[crate::export]
+pub fn sql_like_escape(src: &str) -> String {
+    src.replace('%', r"\%").replace('_', r"\_")
+}
+
+#[crate::export]
+pub fn safe_for_sql(src: &str) -> bool {
+    !src.contains([
+        '\0', '\x08', '\x09', '\x1a', '\n', '\r', '"', '\'', '\\', '%',
+    ])
+}
+
+#[cfg(test)]
+mod unit_test {
+    use super::{safe_for_sql, sql_like_escape};
+    use pretty_assertions::assert_eq;
+
+    #[test]
+    fn sql_like_escape_test() {
+        assert_eq!(sql_like_escape(""), "");
+        assert_eq!(sql_like_escape("abc"), "abc");
+        assert_eq!(sql_like_escape("a%bc"), r"a\%bc");
+        assert_eq!(sql_like_escape("a呼%吸bc"), r"a呼\%吸bc");
+        assert_eq!(sql_like_escape("a呼%吸b%_c"), r"a呼\%吸b\%\_c");
+        assert_eq!(sql_like_escape("_اللغة العربية"), r"\_اللغة العربية");
+    }
+
+    #[test]
+    fn safe_for_sql_test() {
+        assert!(safe_for_sql("123"));
+        assert!(safe_for_sql("人間"));
+        assert!(!safe_for_sql("人間\x09"));
+        assert!(!safe_for_sql("abc\ndef"));
+        assert!(!safe_for_sql("%something%"));
+    }
+}
diff --git a/packages/backend-rs/src/misc/mod.rs b/packages/backend-rs/src/misc/mod.rs
index 7f99a67324..74a483ea51 100644
--- a/packages/backend-rs/src/misc/mod.rs
+++ b/packages/backend-rs/src/misc/mod.rs
@@ -1,6 +1,7 @@
 pub mod acct;
 pub mod check_word_mute;
 pub mod convert_host;
+pub mod escape_sql;
 pub mod format_milliseconds;
 pub mod mastodon_id;
 pub mod meta;
diff --git a/packages/backend/src/misc/safe-for-sql.ts b/packages/backend/src/misc/safe-for-sql.ts
deleted file mode 100644
index 02eb7f0a26..0000000000
--- a/packages/backend/src/misc/safe-for-sql.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-export function safeForSql(text: string): boolean {
-	return !/[\0\x08\x09\x1a\n\r"'\\\%]/g.test(text);
-}
diff --git a/packages/backend/src/misc/sql-like-escape.ts b/packages/backend/src/misc/sql-like-escape.ts
deleted file mode 100644
index 453947d6ec..0000000000
--- a/packages/backend/src/misc/sql-like-escape.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-export function sqlLikeEscape(s: string) {
-	return s.replace(/([%_])/g, "\\$1");
-}
diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/list-remote.ts b/packages/backend/src/server/api/endpoints/admin/emoji/list-remote.ts
index 5c3e19d9e0..9c7a5180d3 100644
--- a/packages/backend/src/server/api/endpoints/admin/emoji/list-remote.ts
+++ b/packages/backend/src/server/api/endpoints/admin/emoji/list-remote.ts
@@ -1,8 +1,7 @@
 import define from "@/server/api/define.js";
 import { Emojis } from "@/models/index.js";
-import { toPuny } from "backend-rs";
+import { sqlLikeEscape, toPuny } from "backend-rs";
 import { makePaginationQuery } from "@/server/api/common/make-pagination-query.js";
-import { sqlLikeEscape } from "@/misc/sql-like-escape.js";
 import { ApiError } from "@/server/api/error.js";
 
 export const meta = {
diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/list.ts b/packages/backend/src/server/api/endpoints/admin/emoji/list.ts
index 434b679608..98a69090db 100644
--- a/packages/backend/src/server/api/endpoints/admin/emoji/list.ts
+++ b/packages/backend/src/server/api/endpoints/admin/emoji/list.ts
@@ -1,8 +1,8 @@
 import define from "@/server/api/define.js";
 import { Emojis } from "@/models/index.js";
-import { makePaginationQuery } from "../../../common/make-pagination-query.js";
+import { makePaginationQuery } from "@/server/api/common/make-pagination-query.js";
 import type { Emoji } from "@/models/entities/emoji.js";
-//import { sqlLikeEscape } from "@/misc/sql-like-escape.js";
+//import { sqlLikeEscape } from "backend-rs";
 import { ApiError } from "@/server/api/error.js";
 
 export const meta = {
diff --git a/packages/backend/src/server/api/endpoints/admin/show-users.ts b/packages/backend/src/server/api/endpoints/admin/show-users.ts
index 1e6ebeda93..8a892c3606 100644
--- a/packages/backend/src/server/api/endpoints/admin/show-users.ts
+++ b/packages/backend/src/server/api/endpoints/admin/show-users.ts
@@ -1,6 +1,6 @@
 import { Users } from "@/models/index.js";
 import define from "@/server/api/define.js";
-import { sqlLikeEscape } from "@/misc/sql-like-escape.js";
+import { sqlLikeEscape } from "backend-rs";
 
 export const meta = {
 	tags: ["admin"],
diff --git a/packages/backend/src/server/api/endpoints/channels/search.ts b/packages/backend/src/server/api/endpoints/channels/search.ts
index b2fab701c5..ed44250a37 100644
--- a/packages/backend/src/server/api/endpoints/channels/search.ts
+++ b/packages/backend/src/server/api/endpoints/channels/search.ts
@@ -2,7 +2,7 @@ import define from "@/server/api/define.js";
 import { Brackets } from "typeorm";
 import { makePaginationQuery } from "@/server/api/common/make-pagination-query.js";
 import { Channels } from "@/models/index.js";
-import { sqlLikeEscape } from "@/misc/sql-like-escape.js";
+import { sqlLikeEscape } from "backend-rs";
 
 export const meta = {
 	tags: ["channels"],
diff --git a/packages/backend/src/server/api/endpoints/federation/instances.ts b/packages/backend/src/server/api/endpoints/federation/instances.ts
index 8c021d0e65..362ab098fb 100644
--- a/packages/backend/src/server/api/endpoints/federation/instances.ts
+++ b/packages/backend/src/server/api/endpoints/federation/instances.ts
@@ -1,7 +1,6 @@
 import define from "@/server/api/define.js";
 import { Instances } from "@/models/index.js";
-import { fetchMeta } from "backend-rs";
-import { sqlLikeEscape } from "@/misc/sql-like-escape.js";
+import { fetchMeta, sqlLikeEscape } from "backend-rs";
 
 export const meta = {
 	tags: ["federation"],
diff --git a/packages/backend/src/server/api/endpoints/hashtags/search.ts b/packages/backend/src/server/api/endpoints/hashtags/search.ts
index 1dc1fb4922..8fb5b23f62 100644
--- a/packages/backend/src/server/api/endpoints/hashtags/search.ts
+++ b/packages/backend/src/server/api/endpoints/hashtags/search.ts
@@ -1,6 +1,6 @@
 import define from "@/server/api/define.js";
 import { Hashtags } from "@/models/index.js";
-import { sqlLikeEscape } from "@/misc/sql-like-escape.js";
+import { sqlLikeEscape } from "backend-rs";
 
 export const meta = {
 	tags: ["hashtags"],
diff --git a/packages/backend/src/server/api/endpoints/hashtags/trend.ts b/packages/backend/src/server/api/endpoints/hashtags/trend.ts
index 9d31445a42..531a494248 100644
--- a/packages/backend/src/server/api/endpoints/hashtags/trend.ts
+++ b/packages/backend/src/server/api/endpoints/hashtags/trend.ts
@@ -1,9 +1,8 @@
 import { Brackets } from "typeorm";
 import define from "@/server/api/define.js";
-import { fetchMeta } from "backend-rs";
+import { fetchMeta, safeForSql } from "backend-rs";
 import { Notes } from "@/models/index.js";
 import type { Note } from "@/models/entities/note.js";
-import { safeForSql } from "@/misc/safe-for-sql.js";
 import { normalizeForSearch } from "@/misc/normalize-for-search.js";
 
 /*
diff --git a/packages/backend/src/server/api/endpoints/notes/search-by-tag.ts b/packages/backend/src/server/api/endpoints/notes/search-by-tag.ts
index e87725e342..f449ea081a 100644
--- a/packages/backend/src/server/api/endpoints/notes/search-by-tag.ts
+++ b/packages/backend/src/server/api/endpoints/notes/search-by-tag.ts
@@ -1,6 +1,6 @@
 import { Brackets } from "typeorm";
 import { Notes } from "@/models/index.js";
-import { safeForSql } from "@/misc/safe-for-sql.js";
+import { safeForSql } from "backend-rs";
 import { normalizeForSearch } from "@/misc/normalize-for-search.js";
 import define from "@/server/api/define.js";
 import { makePaginationQuery } from "@/server/api/common/make-pagination-query.js";
diff --git a/packages/backend/src/server/api/endpoints/notes/search.ts b/packages/backend/src/server/api/endpoints/notes/search.ts
index b159a91944..f28208cba9 100644
--- a/packages/backend/src/server/api/endpoints/notes/search.ts
+++ b/packages/backend/src/server/api/endpoints/notes/search.ts
@@ -1,11 +1,11 @@
 import { Notes } from "@/models/index.js";
-import { Note } from "@/models/entities/note.js";
+import type { Note } from "@/models/entities/note.js";
 import define from "@/server/api/define.js";
 import { makePaginationQuery } from "@/server/api/common/make-pagination-query.js";
 import { generateVisibilityQuery } from "@/server/api/common/generate-visibility-query.js";
 import { generateMutedUserQuery } from "@/server/api/common/generate-muted-user-query.js";
 import { generateBlockedUserQuery } from "@/server/api/common/generate-block-query.js";
-import { sqlLikeEscape } from "@/misc/sql-like-escape.js";
+import { sqlLikeEscape } from "backend-rs";
 import type { SelectQueryBuilder } from "typeorm";
 
 export const meta = {
diff --git a/packages/backend/src/server/api/endpoints/users/search-by-username-and-host.ts b/packages/backend/src/server/api/endpoints/users/search-by-username-and-host.ts
index 517ef615b1..fe15ae18c0 100644
--- a/packages/backend/src/server/api/endpoints/users/search-by-username-and-host.ts
+++ b/packages/backend/src/server/api/endpoints/users/search-by-username-and-host.ts
@@ -2,7 +2,7 @@ import { Brackets } from "typeorm";
 import { Followings, Users } from "@/models/index.js";
 import type { User } from "@/models/entities/user.js";
 import define from "@/server/api/define.js";
-import { sqlLikeEscape } from "@/misc/sql-like-escape.js";
+import { sqlLikeEscape } from "backend-rs";
 
 export const meta = {
 	tags: ["users"],
diff --git a/packages/backend/src/server/api/endpoints/users/search.ts b/packages/backend/src/server/api/endpoints/users/search.ts
index a15a0feb4b..df0701709b 100644
--- a/packages/backend/src/server/api/endpoints/users/search.ts
+++ b/packages/backend/src/server/api/endpoints/users/search.ts
@@ -2,7 +2,7 @@ import { Brackets } from "typeorm";
 import { UserProfiles, Users } from "@/models/index.js";
 import type { User } from "@/models/entities/user.js";
 import define from "@/server/api/define.js";
-import { sqlLikeEscape } from "@/misc/sql-like-escape.js";
+import { sqlLikeEscape } from "backend-rs";
 
 export const meta = {
 	tags: ["users"],

From 21225f713716e72427c7cf05f0e55401e80cb2c9 Mon Sep 17 00:00:00 2001
From: naskya <m@naskya.net>
Date: Mon, 15 Apr 2024 04:09:33 +0900
Subject: [PATCH 059/110] chore: update dependencies

---
 Cargo.lock                    |   81 +--
 Cargo.toml                    |    2 +-
 package.json                  |    8 +-
 packages/backend/package.json |   12 +-
 packages/client/package.json  |    6 +-
 pnpm-lock.yaml                | 1132 +++++++++++++++++++--------------
 6 files changed, 727 insertions(+), 514 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock
index 6552351a3a..bc584faf24 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -59,9 +59,9 @@ checksum = "250f629c0161ad8107cf89319e990051fae62832fd343083bea452d93e2205fd"
 
 [[package]]
 name = "allocator-api2"
-version = "0.2.16"
+version = "0.2.18"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0942ffc6dcaadf03badf6e6a2d0228460359d5e34b57ccdc720b7382dfbd5ec5"
+checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f"
 
 [[package]]
 name = "android-tzdata"
@@ -436,9 +436,9 @@ checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9"
 
 [[package]]
 name = "cc"
-version = "1.0.92"
+version = "1.0.94"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2678b2e3449475e95b0aa6f9b506a28e61b3dc8996592b983695e8ebb58a8b41"
+checksum = "17f6e324229dc011159fcc089755d1e2e216a90d43a7dea6853ca740b84f35e7"
 
 [[package]]
 name = "cfg-if"
@@ -464,7 +464,7 @@ dependencies = [
  "num-traits",
  "serde",
  "wasm-bindgen",
- "windows-targets 0.52.4",
+ "windows-targets 0.52.5",
 ]
 
 [[package]]
@@ -695,9 +695,9 @@ checksum = "0d6ef0072f8a535281e4876be788938b528e9a1d43900b82c2569af7da799125"
 
 [[package]]
 name = "either"
-version = "1.10.0"
+version = "1.11.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "11157ac094ffbdde99aa67b23417ebdd801842852b500e395a45a9c0aac03e4a"
+checksum = "a47c1c47d2f5964e29c61246e81db715514cd532db6b5116a25ea3c03d6780a2"
 dependencies = [
  "serde",
 ]
@@ -1246,7 +1246,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "0c2a198fb6b0eada2a8df47933734e6d35d350665a33a3593d7164fa52c75c19"
 dependencies = [
  "cfg-if",
- "windows-targets 0.52.4",
+ "windows-targets 0.52.5",
 ]
 
 [[package]]
@@ -1367,9 +1367,9 @@ dependencies = [
 
 [[package]]
 name = "napi-build"
-version = "2.1.2"
+version = "2.1.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2f9130fccc5f763cf2069b34a089a18f0d0883c66aceb81f2fad541a3d823c43"
+checksum = "e1c0f5d67ee408a4685b61f5ab7e58605c8ae3f2b4189f0127d804ff13d5560a"
 
 [[package]]
 name = "napi-derive"
@@ -1421,9 +1421,9 @@ dependencies = [
 
 [[package]]
 name = "num"
-version = "0.4.1"
+version = "0.4.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b05180d69e3da0e530ba2a1dae5110317e49e3b7f3d41be227dc5f92e49ee7af"
+checksum = "3135b08af27d103b0a51f2ae0f8632117b7b185ccf931445affa8df530576a41"
 dependencies = [
  "num-bigint",
  "num-complex",
@@ -3123,7 +3123,7 @@ version = "0.52.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9"
 dependencies = [
- "windows-targets 0.52.4",
+ "windows-targets 0.52.5",
 ]
 
 [[package]]
@@ -3141,7 +3141,7 @@ version = "0.52.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
 dependencies = [
- "windows-targets 0.52.4",
+ "windows-targets 0.52.5",
 ]
 
 [[package]]
@@ -3161,17 +3161,18 @@ dependencies = [
 
 [[package]]
 name = "windows-targets"
-version = "0.52.4"
+version = "0.52.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7dd37b7e5ab9018759f893a1952c9420d060016fc19a472b4bb20d1bdd694d1b"
+checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb"
 dependencies = [
- "windows_aarch64_gnullvm 0.52.4",
- "windows_aarch64_msvc 0.52.4",
- "windows_i686_gnu 0.52.4",
- "windows_i686_msvc 0.52.4",
- "windows_x86_64_gnu 0.52.4",
- "windows_x86_64_gnullvm 0.52.4",
- "windows_x86_64_msvc 0.52.4",
+ "windows_aarch64_gnullvm 0.52.5",
+ "windows_aarch64_msvc 0.52.5",
+ "windows_i686_gnu 0.52.5",
+ "windows_i686_gnullvm",
+ "windows_i686_msvc 0.52.5",
+ "windows_x86_64_gnu 0.52.5",
+ "windows_x86_64_gnullvm 0.52.5",
+ "windows_x86_64_msvc 0.52.5",
 ]
 
 [[package]]
@@ -3182,9 +3183,9 @@ checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
 
 [[package]]
 name = "windows_aarch64_gnullvm"
-version = "0.52.4"
+version = "0.52.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bcf46cf4c365c6f2d1cc93ce535f2c8b244591df96ceee75d8e83deb70a9cac9"
+checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263"
 
 [[package]]
 name = "windows_aarch64_msvc"
@@ -3194,9 +3195,9 @@ checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
 
 [[package]]
 name = "windows_aarch64_msvc"
-version = "0.52.4"
+version = "0.52.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "da9f259dd3bcf6990b55bffd094c4f7235817ba4ceebde8e6d11cd0c5633b675"
+checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6"
 
 [[package]]
 name = "windows_i686_gnu"
@@ -3206,9 +3207,15 @@ checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
 
 [[package]]
 name = "windows_i686_gnu"
-version = "0.52.4"
+version = "0.52.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b474d8268f99e0995f25b9f095bc7434632601028cf86590aea5c8a5cb7801d3"
+checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670"
+
+[[package]]
+name = "windows_i686_gnullvm"
+version = "0.52.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9"
 
 [[package]]
 name = "windows_i686_msvc"
@@ -3218,9 +3225,9 @@ checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
 
 [[package]]
 name = "windows_i686_msvc"
-version = "0.52.4"
+version = "0.52.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1515e9a29e5bed743cb4415a9ecf5dfca648ce85ee42e15873c3cd8610ff8e02"
+checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf"
 
 [[package]]
 name = "windows_x86_64_gnu"
@@ -3230,9 +3237,9 @@ checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
 
 [[package]]
 name = "windows_x86_64_gnu"
-version = "0.52.4"
+version = "0.52.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5eee091590e89cc02ad514ffe3ead9eb6b660aedca2183455434b93546371a03"
+checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9"
 
 [[package]]
 name = "windows_x86_64_gnullvm"
@@ -3242,9 +3249,9 @@ checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
 
 [[package]]
 name = "windows_x86_64_gnullvm"
-version = "0.52.4"
+version = "0.52.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "77ca79f2451b49fa9e2af39f0747fe999fcda4f5e241b2898624dca97a1f2177"
+checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596"
 
 [[package]]
 name = "windows_x86_64_msvc"
@@ -3254,9 +3261,9 @@ checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
 
 [[package]]
 name = "windows_x86_64_msvc"
-version = "0.52.4"
+version = "0.52.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "32b752e52a2da0ddfbdbcc6fceadfeede4c939ed16d13e648833a61dfb611ed8"
+checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0"
 
 [[package]]
 name = "winnow"
diff --git a/Cargo.toml b/Cargo.toml
index 1a72cb2c5a..c9efe1f69a 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -7,7 +7,7 @@ macro_rs = { path = "packages/macro-rs" }
 
 napi = { version = "2.16.2", default-features = false }
 napi-derive = "2.16.2"
-napi-build = "2.1.2"
+napi-build = "2.1.3"
 
 argon2 = "0.5.3"
 async-trait = "0.1.80"
diff --git a/package.json b/package.json
index a7bb0a5cc1..dd0766bc7b 100644
--- a/package.json
+++ b/package.json
@@ -5,7 +5,7 @@
 		"type": "git",
 		"url": "https://firefish.dev/firefish/firefish.git"
 	},
-	"packageManager": "pnpm@8.15.6",
+	"packageManager": "pnpm@8.15.7",
 	"private": true,
 	"scripts": {
 		"rebuild": "pnpm run clean && pnpm run build",
@@ -36,11 +36,11 @@
 		"clean-all": "pnpm run clean && pnpm run clean-cargo && pnpm run clean-npm"
 	},
 	"dependencies": {
-		"js-yaml": "4.1.0",
 		"gulp": "4.0.2",
 		"gulp-cssnano": "2.1.3",
 		"gulp-replace": "1.1.4",
-		"gulp-terser": "2.1.0"
+		"gulp-terser": "2.1.0",
+		"js-yaml": "4.1.0"
 	},
 	"devDependencies": {
 		"@biomejs/biome": "1.6.4",
@@ -50,7 +50,7 @@
 		"@biomejs/cli-linux-x64": "^1.6.4",
 		"@types/node": "20.12.7",
 		"execa": "8.0.1",
-		"pnpm": "8.15.6",
+		"pnpm": "8.15.7",
 		"typescript": "5.4.5"
 	}
 }
diff --git a/packages/backend/package.json b/packages/backend/package.json
index 5dbf7cad64..bf435ec64b 100644
--- a/packages/backend/package.json
+++ b/packages/backend/package.json
@@ -22,9 +22,9 @@
 		"@swc/core-android-arm64": "1.3.11"
 	},
 	"dependencies": {
-		"@bull-board/api": "5.15.3",
-		"@bull-board/koa": "5.15.3",
-		"@bull-board/ui": "5.15.3",
+		"@bull-board/api": "5.15.5",
+		"@bull-board/koa": "5.15.5",
+		"@bull-board/ui": "5.15.5",
 		"@discordapp/twemoji": "^15.0.3",
 		"@koa/cors": "5.0.0",
 		"@koa/multer": "3.0.2",
@@ -37,7 +37,7 @@
 		"adm-zip": "0.5.10",
 		"ajv": "8.12.0",
 		"archiver": "7.0.1",
-		"aws-sdk": "2.1597.0",
+		"aws-sdk": "2.1599.0",
 		"axios": "^1.6.8",
 		"backend-rs": "workspace:*",
 		"blurhash": "2.0.5",
@@ -52,7 +52,7 @@
 		"date-fns": "3.6.0",
 		"decompress": "^4.2.1",
 		"deep-email-validator": "0.1.21",
-		"deepl-node": "1.12.0",
+		"deepl-node": "1.13.0",
 		"escape-regexp": "0.0.1",
 		"feed": "4.2.2",
 		"file-type": "19.0.0",
@@ -98,7 +98,7 @@
 		"punycode": "2.3.1",
 		"pureimage": "0.4.13",
 		"qrcode": "1.5.3",
-		"qs": "6.12.0",
+		"qs": "6.12.1",
 		"random-seed": "0.3.0",
 		"ratelimiter": "3.4.1",
 		"redis-semaphore": "5.5.1",
diff --git a/packages/client/package.json b/packages/client/package.json
index 594f07c607..97a5f83ef7 100644
--- a/packages/client/package.json
+++ b/packages/client/package.json
@@ -67,9 +67,9 @@
 		"photoswipe": "5.4.3",
 		"prismjs": "1.29.0",
 		"punycode": "2.3.1",
-		"rollup": "4.14.1",
+		"rollup": "4.14.2",
 		"s-age": "1.1.2",
-		"sass": "1.74.1",
+		"sass": "1.75.0",
 		"seedrandom": "3.0.5",
 		"stringz": "2.1.0",
 		"swiper": "11.1.1",
@@ -88,6 +88,6 @@
 		"vue-draggable-plus": "^0.4.0",
 		"vue-plyr": "^7.0.0",
 		"vue-prism-editor": "2.0.0-alpha.2",
-		"vue-tsc": "2.0.12"
+		"vue-tsc": "2.0.13"
 	}
 }
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 02b473ddce..53b6089f9c 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -46,8 +46,8 @@ importers:
         specifier: 8.0.1
         version: 8.0.1
       pnpm:
-        specifier: 8.15.6
-        version: 8.15.6
+        specifier: 8.15.7
+        version: 8.15.7
       typescript:
         specifier: 5.4.5
         version: 5.4.5
@@ -55,14 +55,14 @@ importers:
   packages/backend:
     dependencies:
       '@bull-board/api':
-        specifier: 5.15.3
-        version: 5.15.3(@bull-board/ui@5.15.3)
+        specifier: 5.15.5
+        version: 5.15.5(@bull-board/ui@5.15.5)
       '@bull-board/koa':
-        specifier: 5.15.3
-        version: 5.15.3(@types/koa@2.15.0)(pug@3.0.2)
+        specifier: 5.15.5
+        version: 5.15.5(@types/koa@2.15.0)(pug@3.0.2)
       '@bull-board/ui':
-        specifier: 5.15.3
-        version: 5.15.3
+        specifier: 5.15.5
+        version: 5.15.5
       '@discordapp/twemoji':
         specifier: ^15.0.3
         version: 15.0.3
@@ -100,8 +100,8 @@ importers:
         specifier: 7.0.1
         version: 7.0.1
       aws-sdk:
-        specifier: 2.1597.0
-        version: 2.1597.0
+        specifier: 2.1599.0
+        version: 2.1599.0
       axios:
         specifier: ^1.6.8
         version: 1.6.8
@@ -145,8 +145,8 @@ importers:
         specifier: 0.1.21
         version: 0.1.21
       deepl-node:
-        specifier: 1.12.0
-        version: 1.12.0
+        specifier: 1.13.0
+        version: 1.13.0
       escape-regexp:
         specifier: 0.0.1
         version: 0.0.1
@@ -283,8 +283,8 @@ importers:
         specifier: 1.5.3
         version: 1.5.3
       qs:
-        specifier: 6.12.0
-        version: 6.12.0
+        specifier: 6.12.1
+        version: 6.12.1
       random-seed:
         specifier: 0.3.0
         version: 0.3.0
@@ -547,22 +547,22 @@ importers:
     devDependencies:
       '@eslint-sets/eslint-config-vue3':
         specifier: ^5.12.0
-        version: 5.12.0(@babel/core@7.23.2)(eslint@8.57.0)(prettier@3.2.5)(typescript@5.4.5)
+        version: 5.12.0(@babel/core@7.24.4)(eslint@9.0.0)(prettier@3.2.5)(typescript@5.4.5)
       '@eslint-sets/eslint-config-vue3-ts':
         specifier: ^3.3.0
-        version: 3.3.0(@babel/core@7.23.2)(eslint@8.57.0)(prettier@3.2.5)(typescript@5.4.5)
+        version: 3.3.0(@babel/core@7.24.4)(eslint@9.0.0)(prettier@3.2.5)(typescript@5.4.5)
       '@phosphor-icons/web':
         specifier: ^2.1.1
         version: 2.1.1
       '@rollup/plugin-alias':
         specifier: 5.1.0
-        version: 5.1.0(rollup@4.14.1)
+        version: 5.1.0(rollup@4.14.2)
       '@rollup/plugin-json':
         specifier: 6.1.0
-        version: 6.1.0(rollup@4.14.1)
+        version: 6.1.0(rollup@4.14.2)
       '@rollup/pluginutils':
         specifier: ^5.1.0
-        version: 5.1.0(rollup@4.14.1)
+        version: 5.1.0(rollup@4.14.2)
       '@syuilo/aiscript':
         specifier: 0.17.0
         version: 0.17.0
@@ -658,7 +658,7 @@ importers:
         version: 3.0.12
       eslint-plugin-file-progress:
         specifier: ^1.3.0
-        version: 1.3.0(eslint@8.57.0)
+        version: 1.3.0(eslint@9.0.0)
       eventemitter3:
         specifier: 5.0.1
         version: 5.0.1
@@ -711,14 +711,14 @@ importers:
         specifier: 2.3.1
         version: 2.3.1
       rollup:
-        specifier: 4.14.1
-        version: 4.14.1
+        specifier: 4.14.2
+        version: 4.14.2
       s-age:
         specifier: 1.1.2
         version: 1.1.2
       sass:
-        specifier: 1.74.1
-        version: 1.74.1
+        specifier: 1.75.0
+        version: 1.75.0
       seedrandom:
         specifier: 3.0.5
         version: 3.0.5
@@ -757,7 +757,7 @@ importers:
         version: 9.0.1
       vite:
         specifier: 5.2.8
-        version: 5.2.8(@types/node@20.12.7)(sass@1.74.1)
+        version: 5.2.8(@types/node@20.12.7)(sass@1.75.0)
       vite-plugin-compression:
         specifier: ^0.5.1
         version: 0.5.1(vite@5.2.8)
@@ -766,7 +766,7 @@ importers:
         version: 3.4.21(typescript@5.4.5)
       vue-draggable-plus:
         specifier: ^0.4.0
-        version: 0.4.0(@types/sortablejs@1.15.4)
+        version: 0.4.0(@types/sortablejs@1.15.8)
       vue-plyr:
         specifier: ^7.0.0
         version: 7.0.0
@@ -774,8 +774,8 @@ importers:
         specifier: 2.0.0-alpha.2
         version: 2.0.0-alpha.2(vue@3.4.21)
       vue-tsc:
-        specifier: 2.0.12
-        version: 2.0.12(typescript@5.4.5)
+        specifier: 2.0.13
+        version: 2.0.13(typescript@5.4.5)
 
   packages/firefish-js:
     dependencies:
@@ -931,7 +931,7 @@ importers:
         version: 4.17.21
       ts-jest:
         specifier: ^29.0.5
-        version: 29.1.1(@babel/core@7.23.2)(jest@29.7.0)(typescript@4.9.4)
+        version: 29.1.1(@babel/core@7.24.4)(jest@29.7.0)(typescript@4.9.4)
       typedoc:
         specifier: ^0.23.24
         version: 0.23.28(typescript@4.9.4)
@@ -949,7 +949,7 @@ importers:
         version: 6.2.1
       vite:
         specifier: 5.2.8
-        version: 5.2.8(@types/node@20.12.7)(sass@1.74.1)
+        version: 5.2.8(@types/node@20.12.7)(sass@1.75.0)
       vite-plugin-compression:
         specifier: ^0.5.1
         version: 0.5.1(vite@5.2.8)
@@ -968,6 +968,14 @@ packages:
       '@jridgewell/gen-mapping': 0.3.3
       '@jridgewell/trace-mapping': 0.3.20
 
+  /@ampproject/remapping@2.3.0:
+    resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==}
+    engines: {node: '>=6.0.0'}
+    dependencies:
+      '@jridgewell/gen-mapping': 0.3.5
+      '@jridgewell/trace-mapping': 0.3.25
+    dev: true
+
   /@babel/code-frame@7.22.10:
     resolution: {integrity: sha512-/KKIMG4UEL35WmI9OlvMhurwtytjvXoFcGNrOvyG9zIzA8YmPjVtIZUf7b05+TPO7G7/GEmLHDaoCgACHl9hhA==}
     engines: {node: '>=6.9.0'}
@@ -983,6 +991,14 @@ packages:
       chalk: 2.4.2
     dev: true
 
+  /@babel/code-frame@7.24.2:
+    resolution: {integrity: sha512-y5+tLQyV8pg3fsiln67BVLD1P13Eg4lh5RW9mF0zUuvLrv9uIQ4MCL+CRT+FTsBlBjcIan6PGsLcBN0m3ClUyQ==}
+    engines: {node: '>=6.9.0'}
+    dependencies:
+      '@babel/highlight': 7.24.2
+      picocolors: 1.0.0
+    dev: true
+
   /@babel/compat-data@7.22.9:
     resolution: {integrity: sha512-5UamI7xkUcJ3i9qVDS+KFDEK8/7oJ55/sJMB1Ge7IEapr7KfdfV/HErR+koZwOfd+SgtFKOKRhRakdg++DcJpQ==}
     engines: {node: '>=6.9.0'}
@@ -992,6 +1008,11 @@ packages:
     engines: {node: '>=6.9.0'}
     dev: true
 
+  /@babel/compat-data@7.24.4:
+    resolution: {integrity: sha512-vg8Gih2MLK+kOkHJp4gBEIkyaIi00jgWot2D9QOmmfLC8jINSOzmCLta6Bvz/JSBCqnegV0L80jhxkol5GWNfQ==}
+    engines: {node: '>=6.9.0'}
+    dev: true
+
   /@babel/core@7.22.10:
     resolution: {integrity: sha512-fTmqbbUBAwCcre6zPzNngvsI0aNrPZe77AeqvDxWM9Nm+04RrJ3CAmGHA9f7lJQY6ZMhRztNemy4uslDxTX4Qw==}
     engines: {node: '>=6.9.0'}
@@ -1037,30 +1058,53 @@ packages:
       - supports-color
     dev: true
 
-  /@babel/eslint-parser@7.23.10(@babel/core@7.23.2)(eslint@8.57.0):
+  /@babel/core@7.24.4:
+    resolution: {integrity: sha512-MBVlMXP+kkl5394RBLSxxk/iLTeVGuXTV3cIDXavPpMMqnSnt6apKgan/U8O3USWZCWZT/TbgfEpKa4uMgN4Dg==}
+    engines: {node: '>=6.9.0'}
+    dependencies:
+      '@ampproject/remapping': 2.3.0
+      '@babel/code-frame': 7.24.2
+      '@babel/generator': 7.24.4
+      '@babel/helper-compilation-targets': 7.23.6
+      '@babel/helper-module-transforms': 7.23.3(@babel/core@7.24.4)
+      '@babel/helpers': 7.24.4
+      '@babel/parser': 7.24.4
+      '@babel/template': 7.24.0
+      '@babel/traverse': 7.24.1
+      '@babel/types': 7.24.0
+      convert-source-map: 2.0.0
+      debug: 4.3.4(supports-color@8.1.1)
+      gensync: 1.0.0-beta.2
+      json5: 2.2.3
+      semver: 6.3.1
+    transitivePeerDependencies:
+      - supports-color
+    dev: true
+
+  /@babel/eslint-parser@7.23.10(@babel/core@7.24.4)(eslint@9.0.0):
     resolution: {integrity: sha512-3wSYDPZVnhseRnxRJH6ZVTNknBz76AEnyC+AYYhasjP3Yy23qz0ERR7Fcd2SHmYuSFJ2kY9gaaDd3vyqU09eSw==}
     engines: {node: ^10.13.0 || ^12.13.0 || >=14.0.0}
     peerDependencies:
       '@babel/core': ^7.11.0
       eslint: ^7.5.0 || ^8.0.0
     dependencies:
-      '@babel/core': 7.23.2
+      '@babel/core': 7.24.4
       '@nicolo-ribaudo/eslint-scope-5-internals': 5.1.1-v1
-      eslint: 8.57.0
+      eslint: 9.0.0
       eslint-visitor-keys: 2.1.0
       semver: 6.3.1
     dev: true
 
-  /@babel/eslint-parser@7.23.3(@babel/core@7.23.2)(eslint@8.57.0):
+  /@babel/eslint-parser@7.23.3(@babel/core@7.24.4)(eslint@9.0.0):
     resolution: {integrity: sha512-9bTuNlyx7oSstodm1cR1bECj4fkiknsDa1YniISkJemMY3DGhJNYBECbe6QD/q54mp2J8VO66jW3/7uP//iFCw==}
     engines: {node: ^10.13.0 || ^12.13.0 || >=14.0.0}
     peerDependencies:
       '@babel/core': ^7.11.0
       eslint: ^7.5.0 || ^8.0.0
     dependencies:
-      '@babel/core': 7.23.2
+      '@babel/core': 7.24.4
       '@nicolo-ribaudo/eslint-scope-5-internals': 5.1.1-v1
-      eslint: 8.57.0
+      eslint: 9.0.0
       eslint-visitor-keys: 2.1.0
       semver: 6.3.1
     dev: true
@@ -1084,6 +1128,16 @@ packages:
       jsesc: 2.5.2
     dev: true
 
+  /@babel/generator@7.24.4:
+    resolution: {integrity: sha512-Xd6+v6SnjWVx/nus+y0l1sxMOTOMBkyL4+BIdbALyatQnAe/SRVjANeDPSCYaX+i1iJmuGSKf3Z+E+V/va1Hvw==}
+    engines: {node: '>=6.9.0'}
+    dependencies:
+      '@babel/types': 7.24.0
+      '@jridgewell/gen-mapping': 0.3.5
+      '@jridgewell/trace-mapping': 0.3.25
+      jsesc: 2.5.2
+    dev: true
+
   /@babel/helper-compilation-targets@7.22.10:
     resolution: {integrity: sha512-JMSwHD4J7SLod0idLq5PKgI+6g/hLD/iuWBq08ZX49xE14VpVEojJ5rHWptpirV2j020MvypRLAXAO50igCJ5Q==}
     engines: {node: '>=6.9.0'}
@@ -1105,6 +1159,17 @@ packages:
       semver: 6.3.1
     dev: true
 
+  /@babel/helper-compilation-targets@7.23.6:
+    resolution: {integrity: sha512-9JB548GZoQVmzrFgp8o7KxdgkTGm6xs9DW0o/Pim72UDjzr5ObUQ6ZzYPqA+g9OTS2bBQoctLJrky0RDCAWRgQ==}
+    engines: {node: '>=6.9.0'}
+    dependencies:
+      '@babel/compat-data': 7.24.4
+      '@babel/helper-validator-option': 7.23.5
+      browserslist: 4.23.0
+      lru-cache: 5.1.1
+      semver: 6.3.1
+    dev: true
+
   /@babel/helper-environment-visitor@7.22.20:
     resolution: {integrity: sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==}
     engines: {node: '>=6.9.0'}
@@ -1148,6 +1213,13 @@ packages:
     dependencies:
       '@babel/types': 7.23.0
 
+  /@babel/helper-module-imports@7.24.3:
+    resolution: {integrity: sha512-viKb0F9f2s0BCS22QSF308z/+1YWKV/76mwt61NBzS5izMzDPwdq1pTrzf+Li3npBWX9KdQbkeCt1jSAM7lZqg==}
+    engines: {node: '>=6.9.0'}
+    dependencies:
+      '@babel/types': 7.24.0
+    dev: true
+
   /@babel/helper-module-transforms@7.22.9(@babel/core@7.22.10):
     resolution: {integrity: sha512-t+WA2Xn5K+rTeGtC8jCsdAH52bjggG5TKRuRrAGNM/mjIbO4GxvlLMFOEz9wXY5I2XQ60PMFsAG2WIcG82dQMQ==}
     engines: {node: '>=6.9.0'}
@@ -1175,6 +1247,20 @@ packages:
       '@babel/helper-validator-identifier': 7.22.20
     dev: true
 
+  /@babel/helper-module-transforms@7.23.3(@babel/core@7.24.4):
+    resolution: {integrity: sha512-7bBs4ED9OmswdfDzpz4MpWgSrV7FXlc3zIagvLFjS5H+Mk7Snr21vQ6QwrsoCGMfNC4e4LQPdoULEt4ykz0SRQ==}
+    engines: {node: '>=6.9.0'}
+    peerDependencies:
+      '@babel/core': ^7.0.0
+    dependencies:
+      '@babel/core': 7.24.4
+      '@babel/helper-environment-visitor': 7.22.20
+      '@babel/helper-module-imports': 7.24.3
+      '@babel/helper-simple-access': 7.22.5
+      '@babel/helper-split-export-declaration': 7.22.6
+      '@babel/helper-validator-identifier': 7.22.20
+    dev: true
+
   /@babel/helper-plugin-utils@7.22.5:
     resolution: {integrity: sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg==}
     engines: {node: '>=6.9.0'}
@@ -1195,6 +1281,11 @@ packages:
     resolution: {integrity: sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==}
     engines: {node: '>=6.9.0'}
 
+  /@babel/helper-string-parser@7.24.1:
+    resolution: {integrity: sha512-2ofRCjnnA9y+wk8b9IAREroeUP02KHp431N2mhKniy2yKIDKpbrHv9eXwm8cBeWQYcJmzv5qKCu65P47eCF7CQ==}
+    engines: {node: '>=6.9.0'}
+    dev: true
+
   /@babel/helper-validator-identifier@7.22.20:
     resolution: {integrity: sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==}
     engines: {node: '>=6.9.0'}
@@ -1212,6 +1303,11 @@ packages:
     resolution: {integrity: sha512-R3oB6xlIVKUnxNUxbmgq7pKjxpru24zlimpE8WK47fACIlM0II/Hm1RS8IaOI7NgCr6LNS+jl5l75m20npAziw==}
     engines: {node: '>=6.9.0'}
 
+  /@babel/helper-validator-option@7.23.5:
+    resolution: {integrity: sha512-85ttAOMLsr53VgXkTbkx8oA6YTfT4q7/HzXSLEYmjcSTJPMPQtvq1BD79Byep5xMUYbGRzEpDsjUf3dyp54IKw==}
+    engines: {node: '>=6.9.0'}
+    dev: true
+
   /@babel/helpers@7.22.10:
     resolution: {integrity: sha512-a41J4NW8HyZa1I1vAndrraTlPZ/eZoga2ZgS7fEr0tZJGVU4xqdE80CEm0CcNjha5EZ8fTBYLKHF0kqDUuAwQw==}
     engines: {node: '>=6.9.0'}
@@ -1233,6 +1329,17 @@ packages:
       - supports-color
     dev: true
 
+  /@babel/helpers@7.24.4:
+    resolution: {integrity: sha512-FewdlZbSiwaVGlgT1DPANDuCHaDMiOo+D/IDYRFYjHOuv66xMSJ7fQwwODwRNAPkADIO/z1EoF/l2BCWlWABDw==}
+    engines: {node: '>=6.9.0'}
+    dependencies:
+      '@babel/template': 7.24.0
+      '@babel/traverse': 7.24.1
+      '@babel/types': 7.24.0
+    transitivePeerDependencies:
+      - supports-color
+    dev: true
+
   /@babel/highlight@7.22.10:
     resolution: {integrity: sha512-78aUtVcT7MUscr0K5mIEnkwxPE0MaxkR5RxRwuHaQ+JuU5AmTPhY+do2mdzVTnIJJpyBglql2pehuBIWHug+WQ==}
     engines: {node: '>=6.9.0'}
@@ -1250,6 +1357,16 @@ packages:
       js-tokens: 4.0.0
     dev: true
 
+  /@babel/highlight@7.24.2:
+    resolution: {integrity: sha512-Yac1ao4flkTxTteCDZLEvdxg2fZfz1v8M4QpaGypq/WPDqg3ijHYbDfs+LG5hvzSoqaSZ9/Z9lKSP3CjZjv+pA==}
+    engines: {node: '>=6.9.0'}
+    dependencies:
+      '@babel/helper-validator-identifier': 7.22.20
+      chalk: 2.4.2
+      js-tokens: 4.0.0
+      picocolors: 1.0.0
+    dev: true
+
   /@babel/parser@7.22.10:
     resolution: {integrity: sha512-lNbdGsQb9ekfsnjFGhEiF4hfFqGgfOP3H3d27re3n+CGhNuTSUEQdfWk556sTLNTloczcdM5TYF2LhzmDQKyvQ==}
     engines: {node: '>=6.0.0'}
@@ -1272,6 +1389,14 @@ packages:
       '@babel/types': 7.23.0
     dev: true
 
+  /@babel/parser@7.24.4:
+    resolution: {integrity: sha512-zTvEBcghmeBma9QIGunWevvBAp4/Qu9Bdq+2k0Ot4fVMD6v3dsC9WOcRSKk7tRRyBM/53yKMJko9xOatGQAwSg==}
+    engines: {node: '>=6.0.0'}
+    hasBin: true
+    dependencies:
+      '@babel/types': 7.24.0
+    dev: true
+
   /@babel/plugin-proposal-export-namespace-from@7.18.9(@babel/core@7.22.10):
     resolution: {integrity: sha512-k1NtHyOMvlDDFeb9G5PhUXuGj8m/wiwojgQVEhJ/fsVsMCpLyOP4h0uGEjYJKrRI+EVPlb5Jk+Gt9P97lOGwtA==}
     engines: {node: '>=6.9.0'}
@@ -1457,6 +1582,15 @@ packages:
       '@babel/parser': 7.23.0
       '@babel/types': 7.23.0
 
+  /@babel/template@7.24.0:
+    resolution: {integrity: sha512-Bkf2q8lMB0AFpX0NFEqSbx1OkTHf0f+0j82mkw+ZpzBnkk7e9Ql0891vlfgi+kHwOk8tQjiQHpqh4LaSa0fKEA==}
+    engines: {node: '>=6.9.0'}
+    dependencies:
+      '@babel/code-frame': 7.24.2
+      '@babel/parser': 7.24.4
+      '@babel/types': 7.24.0
+    dev: true
+
   /@babel/traverse@7.22.10:
     resolution: {integrity: sha512-Q/urqV4pRByiNNpb/f5OSv28ZlGJiFiiTh+GAHktbIrkPhPbl90+uW6SmpoLyZqutrg9AEaEf3Q/ZBRHBXgxig==}
     engines: {node: '>=6.9.0'}
@@ -1492,6 +1626,24 @@ packages:
       - supports-color
     dev: true
 
+  /@babel/traverse@7.24.1:
+    resolution: {integrity: sha512-xuU6o9m68KeqZbQuDt2TcKSxUw/mrsvavlEqQ1leZ/B+C9tk6E4sRWy97WaXgvq5E+nU3cXMxv3WKOCanVMCmQ==}
+    engines: {node: '>=6.9.0'}
+    dependencies:
+      '@babel/code-frame': 7.24.2
+      '@babel/generator': 7.24.4
+      '@babel/helper-environment-visitor': 7.22.20
+      '@babel/helper-function-name': 7.23.0
+      '@babel/helper-hoist-variables': 7.22.5
+      '@babel/helper-split-export-declaration': 7.22.6
+      '@babel/parser': 7.24.4
+      '@babel/types': 7.24.0
+      debug: 4.3.4(supports-color@8.1.1)
+      globals: 11.12.0
+    transitivePeerDependencies:
+      - supports-color
+    dev: true
+
   /@babel/types@7.22.10:
     resolution: {integrity: sha512-obaoigiLrlDZ7TUQln/8m4mSqIW2QFeOrCQc9r+xsaHGNoplVNYlRVpsfE8Vj35GEm2ZH4ZhrNYogs/3fj85kg==}
     engines: {node: '>=6.9.0'}
@@ -1508,6 +1660,15 @@ packages:
       '@babel/helper-validator-identifier': 7.22.20
       to-fast-properties: 2.0.0
 
+  /@babel/types@7.24.0:
+    resolution: {integrity: sha512-+j7a5c253RfKh8iABBhywc8NSfP5LURe7Uh4qpsh6jc+aLJguvmIUBdjSdEMQv2bENrCR5MfRdjGo7vzS/ob7w==}
+    engines: {node: '>=6.9.0'}
+    dependencies:
+      '@babel/helper-string-parser': 7.24.1
+      '@babel/helper-validator-identifier': 7.22.20
+      to-fast-properties: 2.0.0
+    dev: true
+
   /@bcoe/v8-coverage@0.2.3:
     resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==}
     dev: true
@@ -1592,26 +1753,26 @@ packages:
     dev: true
     optional: true
 
-  /@bull-board/api@5.15.3(@bull-board/ui@5.15.3):
-    resolution: {integrity: sha512-tlEYOI6Hp0ZGozDCtKQEFgvzTKXj+drKStHJm86s1TcUZlsnMjzR0BUxu5CW6EB3tS3MtPLJH5RQCmUq0UEiiQ==}
+  /@bull-board/api@5.15.5(@bull-board/ui@5.15.5):
+    resolution: {integrity: sha512-s3x0f+0s4nwndBM+QSROMVKiDyE/vaaouQCsxRWOFqneLCkM+Ro2wF6fkhmFkZMjouoBbS8rCFGaIZ+8uttYtg==}
     peerDependencies:
-      '@bull-board/ui': 5.15.3
+      '@bull-board/ui': 5.15.5
     dependencies:
-      '@bull-board/ui': 5.15.3
+      '@bull-board/ui': 5.15.5
       redis-info: 3.1.0
     dev: false
 
-  /@bull-board/koa@5.15.3(@types/koa@2.15.0)(pug@3.0.2):
-    resolution: {integrity: sha512-pgHdRcre8RJKwWqlMLFY1oj742hLtxVrHsT2s4k+Ribzmoj3bq1tgRHtu6m9TX7AyABBtcTfTo30NNbPPrYR7A==}
+  /@bull-board/koa@5.15.5(@types/koa@2.15.0)(pug@3.0.2):
+    resolution: {integrity: sha512-Kbmca8hKNW5wLpGM/H1RBm09bcdK+KWCsINUyDtp91bGwMRK0mhiqvjJLJpRohXXmtPTnnJDuVO9p1gYsbed3Q==}
     dependencies:
-      '@bull-board/api': 5.15.3(@bull-board/ui@5.15.3)
-      '@bull-board/ui': 5.15.3
-      ejs: 3.1.9
+      '@bull-board/api': 5.15.5(@bull-board/ui@5.15.5)
+      '@bull-board/ui': 5.15.5
+      ejs: 3.1.10
       koa: 2.15.3
       koa-mount: 4.0.0
       koa-router: 10.1.1
       koa-static: 5.0.0
-      koa-views: 7.0.2(@types/koa@2.15.0)(ejs@3.1.9)(pug@3.0.2)
+      koa-views: 7.0.2(@types/koa@2.15.0)(ejs@3.1.10)(pug@3.0.2)
     transitivePeerDependencies:
       - '@types/koa'
       - arc-templates
@@ -1669,10 +1830,10 @@ packages:
       - whiskers
     dev: false
 
-  /@bull-board/ui@5.15.3:
-    resolution: {integrity: sha512-wCXk+s4cSszZe0p0sYYxZPLSKafFQNPsUypTvpAh3IC2p4fr6F/wUBGb1kBMspRkFC19l5yFCD5qPHVlAR0QKw==}
+  /@bull-board/ui@5.15.5:
+    resolution: {integrity: sha512-TSXgqBDI3ig6ez6yHArGzpwCuA/rhQewv0KOUAvPzssgX4HqfkatrV7gTuTM+XJe7/sLiXnBiryV7SRV0hgRMg==}
     dependencies:
-      '@bull-board/api': 5.15.3(@bull-board/ui@5.15.3)
+      '@bull-board/api': 5.15.5(@bull-board/ui@5.15.5)
     dev: false
 
   /@cbor-extract/cbor-extract-darwin-arm64@2.2.0:
@@ -1841,8 +2002,8 @@ packages:
       universalify: 0.1.2
     dev: false
 
-  /@emnapi/runtime@1.1.0:
-    resolution: {integrity: sha512-gCGlE0fJGWalfy+wbFApjhKn6uoSVvopru77IPyxNKkjkaiSx2HxDS7eOYSmo9dcMIhmmIvoxiC3N9TM1c3EaA==}
+  /@emnapi/runtime@1.1.1:
+    resolution: {integrity: sha512-3bfqkzuR1KLx57nZfjr2NLnFOobvyS0aTszaEGCGqmYMVDRaGvgIZbjGSV/MHSSmLgQ/b9JFHQ5xm5WRZYd+XQ==}
     requiresBuild: true
     dependencies:
       tslib: 2.6.2
@@ -2075,16 +2236,6 @@ packages:
       eslint-visitor-keys: 3.4.3
     dev: true
 
-  /@eslint-community/eslint-utils@4.4.0(eslint@8.57.0):
-    resolution: {integrity: sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==}
-    engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
-    peerDependencies:
-      eslint: ^6.0.0 || ^7.0.0 || >=8.0.0
-    dependencies:
-      eslint: 8.57.0
-      eslint-visitor-keys: 3.4.3
-    dev: true
-
   /@eslint-community/eslint-utils@4.4.0(eslint@9.0.0):
     resolution: {integrity: sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==}
     engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
@@ -2105,29 +2256,29 @@ packages:
     engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0}
     dev: true
 
-  /@eslint-sets/eslint-config-basic@3.3.0(@babel/core@7.23.2)(@typescript-eslint/parser@5.62.0)(eslint@8.57.0)(prettier@3.2.5):
+  /@eslint-sets/eslint-config-basic@3.3.0(@babel/core@7.24.4)(@typescript-eslint/parser@5.62.0)(eslint@9.0.0)(prettier@3.2.5):
     resolution: {integrity: sha512-x5YH0CvZJxn19/5ehu188XaoLQpxOGlFiIuPHCN6FyONgrmriakT/cmIIBOJg2Vi/y1bn2xbhsgVNb00J3HyTg==}
     peerDependencies:
       eslint: '>=8.0.0'
       prettier: '>=2.0.0'
     dependencies:
-      '@babel/eslint-parser': 7.23.3(@babel/core@7.23.2)(eslint@8.57.0)
-      eslint: 8.57.0
-      eslint-config-prettier: 8.9.0(eslint@8.57.0)
-      eslint-plugin-eslint-comments: 3.2.0(eslint@8.57.0)
+      '@babel/eslint-parser': 7.23.3(@babel/core@7.24.4)(eslint@9.0.0)
+      eslint: 9.0.0
+      eslint-config-prettier: 8.9.0(eslint@9.0.0)
+      eslint-plugin-eslint-comments: 3.2.0(eslint@9.0.0)
       eslint-plugin-html: 7.1.0
-      eslint-plugin-import: 2.29.0(@typescript-eslint/parser@5.62.0)(eslint@8.57.0)
-      eslint-plugin-jsonc: 2.10.0(eslint@8.57.0)
-      eslint-plugin-markdown: 3.0.1(eslint@8.57.0)
-      eslint-plugin-n: 15.7.0(eslint@8.57.0)
-      eslint-plugin-prettier: 4.2.1(eslint-config-prettier@8.9.0)(eslint@8.57.0)(prettier@3.2.5)
-      eslint-plugin-promise: 5.2.0(eslint@8.57.0)
+      eslint-plugin-import: 2.29.0(@typescript-eslint/parser@5.62.0)(eslint@9.0.0)
+      eslint-plugin-jsonc: 2.10.0(eslint@9.0.0)
+      eslint-plugin-markdown: 3.0.1(eslint@9.0.0)
+      eslint-plugin-n: 15.7.0(eslint@9.0.0)
+      eslint-plugin-prettier: 4.2.1(eslint-config-prettier@8.9.0)(eslint@9.0.0)(prettier@3.2.5)
+      eslint-plugin-promise: 5.2.0(eslint@9.0.0)
       eslint-plugin-tsdoc: 0.2.17
-      eslint-plugin-unicorn: 45.0.2(eslint@8.57.0)
-      eslint-plugin-yml: 1.10.0(eslint@8.57.0)
+      eslint-plugin-unicorn: 45.0.2(eslint@9.0.0)
+      eslint-plugin-yml: 1.10.0(eslint@9.0.0)
       jsonc-eslint-parser: 2.3.0
       prettier: 3.2.5
-      vue-eslint-parser: 9.3.2(eslint@8.57.0)
+      vue-eslint-parser: 9.3.2(eslint@9.0.0)
       yaml-eslint-parser: 1.2.2
     transitivePeerDependencies:
       - '@babel/core'
@@ -2137,7 +2288,7 @@ packages:
       - supports-color
     dev: true
 
-  /@eslint-sets/eslint-config-basic@5.12.0(@babel/core@7.23.2)(@typescript-eslint/parser@6.21.0)(eslint@8.57.0)(prettier@3.2.5)(typescript@5.4.5):
+  /@eslint-sets/eslint-config-basic@5.12.0(@babel/core@7.24.4)(@typescript-eslint/parser@6.21.0)(eslint@9.0.0)(prettier@3.2.5)(typescript@5.4.5):
     resolution: {integrity: sha512-AgECfmJsiVOWKmvgjv780VuuoT9SE6PRgxGTtytHSfE9b9MAJjHxToVTKtD4UEKvocEGbg2EcwqGbff8cxDWKw==}
     peerDependencies:
       eslint: '>=7.4.0'
@@ -2147,24 +2298,24 @@ packages:
       typescript:
         optional: true
     dependencies:
-      '@babel/eslint-parser': 7.23.10(@babel/core@7.23.2)(eslint@8.57.0)
-      eslint: 8.57.0
-      eslint-config-prettier: 9.1.0(eslint@8.57.0)
-      eslint-plugin-eslint-comments: 3.2.0(eslint@8.57.0)
+      '@babel/eslint-parser': 7.23.10(@babel/core@7.24.4)(eslint@9.0.0)
+      eslint: 9.0.0
+      eslint-config-prettier: 9.1.0(eslint@9.0.0)
+      eslint-plugin-eslint-comments: 3.2.0(eslint@9.0.0)
       eslint-plugin-html: 7.1.0
-      eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.21.0)(eslint@8.57.0)
-      eslint-plugin-jsonc: 2.13.0(eslint@8.57.0)
-      eslint-plugin-markdown: 3.0.1(eslint@8.57.0)
-      eslint-plugin-n: 16.6.2(eslint@8.57.0)
-      eslint-plugin-prettier: 5.1.3(eslint-config-prettier@9.1.0)(eslint@8.57.0)(prettier@3.2.5)
-      eslint-plugin-promise: 6.1.1(eslint@8.57.0)
+      eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.21.0)(eslint@9.0.0)
+      eslint-plugin-jsonc: 2.13.0(eslint@9.0.0)
+      eslint-plugin-markdown: 3.0.1(eslint@9.0.0)
+      eslint-plugin-n: 16.6.2(eslint@9.0.0)
+      eslint-plugin-prettier: 5.1.3(eslint-config-prettier@9.1.0)(eslint@9.0.0)(prettier@3.2.5)
+      eslint-plugin-promise: 6.1.1(eslint@9.0.0)
       eslint-plugin-tsdoc: 0.2.17
-      eslint-plugin-unicorn: 40.1.0(eslint@8.57.0)
-      eslint-plugin-yml: 1.12.2(eslint@8.57.0)
+      eslint-plugin-unicorn: 40.1.0(eslint@9.0.0)
+      eslint-plugin-yml: 1.12.2(eslint@9.0.0)
       jsonc-eslint-parser: 2.4.0
       prettier: 3.2.5
       typescript: 5.4.5
-      vue-eslint-parser: 9.4.2(eslint@8.57.0)
+      vue-eslint-parser: 9.4.2(eslint@9.0.0)
       yaml-eslint-parser: 1.2.2
     transitivePeerDependencies:
       - '@babel/core'
@@ -2175,19 +2326,19 @@ packages:
       - supports-color
     dev: true
 
-  /@eslint-sets/eslint-config-ts@3.3.0(@babel/core@7.23.2)(eslint@8.57.0)(prettier@3.2.5)(typescript@5.4.5):
+  /@eslint-sets/eslint-config-ts@3.3.0(@babel/core@7.24.4)(eslint@9.0.0)(prettier@3.2.5)(typescript@5.4.5):
     resolution: {integrity: sha512-4Vj3KxYx16hmW6AyEv1mil0gVN8H3rdJt8TRWufbAj0ZN+EjwOPf3TqE7ASCYto/NpA8xWQY3NGm/og9Or/dDQ==}
     peerDependencies:
       eslint: '>=8.0.0'
       prettier: '>=2.0.0'
       typescript: '>=4.0.0'
     dependencies:
-      '@eslint-sets/eslint-config-basic': 3.3.0(@babel/core@7.23.2)(@typescript-eslint/parser@5.62.0)(eslint@8.57.0)(prettier@3.2.5)
-      '@typescript-eslint/eslint-plugin': 5.62.0(@typescript-eslint/parser@5.62.0)(eslint@8.57.0)(typescript@5.4.5)
-      '@typescript-eslint/parser': 5.62.0(eslint@8.57.0)(typescript@5.4.5)
-      eslint: 8.57.0
-      eslint-config-prettier: 8.9.0(eslint@8.57.0)
-      eslint-plugin-prettier: 4.2.1(eslint-config-prettier@8.9.0)(eslint@8.57.0)(prettier@3.2.5)
+      '@eslint-sets/eslint-config-basic': 3.3.0(@babel/core@7.24.4)(@typescript-eslint/parser@5.62.0)(eslint@9.0.0)(prettier@3.2.5)
+      '@typescript-eslint/eslint-plugin': 5.62.0(@typescript-eslint/parser@5.62.0)(eslint@9.0.0)(typescript@5.4.5)
+      '@typescript-eslint/parser': 5.62.0(eslint@9.0.0)(typescript@5.4.5)
+      eslint: 9.0.0
+      eslint-config-prettier: 8.9.0(eslint@9.0.0)
+      eslint-plugin-prettier: 4.2.1(eslint-config-prettier@8.9.0)(eslint@9.0.0)(prettier@3.2.5)
       eslint-plugin-tsdoc: 0.2.17
       prettier: 3.2.5
       typescript: 5.4.5
@@ -2198,7 +2349,7 @@ packages:
       - supports-color
     dev: true
 
-  /@eslint-sets/eslint-config-ts@5.12.0(@babel/core@7.23.2)(eslint@8.57.0)(prettier@3.2.5)(typescript@5.4.5):
+  /@eslint-sets/eslint-config-ts@5.12.0(@babel/core@7.24.4)(eslint@9.0.0)(prettier@3.2.5)(typescript@5.4.5):
     resolution: {integrity: sha512-7vOzV6qYv0SbA9W17m9lkG/Zv+qVeCcAbWEY1d9hUbBHx9Ip48kNMNVDrnh97zUORXGcmjxsZ81W2lC36Ox2pw==}
     peerDependencies:
       eslint: '>=7.4.0'
@@ -2208,12 +2359,12 @@ packages:
       typescript:
         optional: true
     dependencies:
-      '@eslint-sets/eslint-config-basic': 5.12.0(@babel/core@7.23.2)(@typescript-eslint/parser@6.21.0)(eslint@8.57.0)(prettier@3.2.5)(typescript@5.4.5)
-      '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0)(eslint@8.57.0)(typescript@5.4.5)
-      '@typescript-eslint/parser': 6.21.0(eslint@8.57.0)(typescript@5.4.5)
-      eslint: 8.57.0
-      eslint-config-prettier: 9.1.0(eslint@8.57.0)
-      eslint-plugin-prettier: 5.1.3(eslint-config-prettier@9.1.0)(eslint@8.57.0)(prettier@3.2.5)
+      '@eslint-sets/eslint-config-basic': 5.12.0(@babel/core@7.24.4)(@typescript-eslint/parser@6.21.0)(eslint@9.0.0)(prettier@3.2.5)(typescript@5.4.5)
+      '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0)(eslint@9.0.0)(typescript@5.4.5)
+      '@typescript-eslint/parser': 6.21.0(eslint@9.0.0)(typescript@5.4.5)
+      eslint: 9.0.0
+      eslint-config-prettier: 9.1.0(eslint@9.0.0)
+      eslint-plugin-prettier: 5.1.3(eslint-config-prettier@9.1.0)(eslint@9.0.0)(prettier@3.2.5)
       eslint-plugin-tsdoc: 0.2.17
       prettier: 3.2.5
       typescript: 5.4.5
@@ -2225,26 +2376,26 @@ packages:
       - supports-color
     dev: true
 
-  /@eslint-sets/eslint-config-vue3-ts@3.3.0(@babel/core@7.23.2)(eslint@8.57.0)(prettier@3.2.5)(typescript@5.4.5):
+  /@eslint-sets/eslint-config-vue3-ts@3.3.0(@babel/core@7.24.4)(eslint@9.0.0)(prettier@3.2.5)(typescript@5.4.5):
     resolution: {integrity: sha512-KX3VFuS5U4FYKfZ6PABQjl54BMpNapNjYYe103Nm2Zy8y9zphDCBAARbhU97XNSvzkurve7HhJcsi9gXrWlGFA==}
     peerDependencies:
       eslint: '>=8.0.0'
       prettier: '>=2.0.0'
       typescript: '>=4.0.0'
     dependencies:
-      '@eslint-sets/eslint-config-ts': 3.3.0(@babel/core@7.23.2)(eslint@8.57.0)(prettier@3.2.5)(typescript@5.4.5)
-      '@typescript-eslint/eslint-plugin': 5.62.0(@typescript-eslint/parser@5.62.0)(eslint@8.57.0)(typescript@5.4.5)
-      '@typescript-eslint/parser': 5.62.0(eslint@8.57.0)(typescript@5.4.5)
-      eslint: 8.57.0
-      eslint-config-prettier: 8.9.0(eslint@8.57.0)
-      eslint-plugin-prettier: 4.2.1(eslint-config-prettier@8.9.0)(eslint@8.57.0)(prettier@3.2.5)
+      '@eslint-sets/eslint-config-ts': 3.3.0(@babel/core@7.24.4)(eslint@9.0.0)(prettier@3.2.5)(typescript@5.4.5)
+      '@typescript-eslint/eslint-plugin': 5.62.0(@typescript-eslint/parser@5.62.0)(eslint@9.0.0)(typescript@5.4.5)
+      '@typescript-eslint/parser': 5.62.0(eslint@9.0.0)(typescript@5.4.5)
+      eslint: 9.0.0
+      eslint-config-prettier: 8.9.0(eslint@9.0.0)
+      eslint-plugin-prettier: 4.2.1(eslint-config-prettier@8.9.0)(eslint@9.0.0)(prettier@3.2.5)
       eslint-plugin-tsdoc: 0.2.17
       eslint-plugin-vitest-globals: 1.4.0
-      eslint-plugin-vue: 9.16.1(eslint@8.57.0)
-      eslint-plugin-vue-scoped-css: 2.5.0(eslint@8.57.0)(vue-eslint-parser@9.3.1)
+      eslint-plugin-vue: 9.16.1(eslint@9.0.0)
+      eslint-plugin-vue-scoped-css: 2.5.0(eslint@9.0.0)(vue-eslint-parser@9.3.1)
       prettier: 3.2.5
       typescript: 5.4.5
-      vue-eslint-parser: 9.3.1(eslint@8.57.0)
+      vue-eslint-parser: 9.3.1(eslint@9.0.0)
     transitivePeerDependencies:
       - '@babel/core'
       - eslint-import-resolver-typescript
@@ -2252,7 +2403,7 @@ packages:
       - supports-color
     dev: true
 
-  /@eslint-sets/eslint-config-vue3@5.12.0(@babel/core@7.23.2)(eslint@8.57.0)(prettier@3.2.5)(typescript@5.4.5):
+  /@eslint-sets/eslint-config-vue3@5.12.0(@babel/core@7.24.4)(eslint@9.0.0)(prettier@3.2.5)(typescript@5.4.5):
     resolution: {integrity: sha512-gQBmQicZihPcxncIdkKagQGZ2dH+97ioAlUpsaczEdgY9pLrLOU5oGTetjbaxAp6zGS2sXm1n0i2BnwRIlt4Bg==}
     peerDependencies:
       eslint: '>=7.4.0'
@@ -2262,22 +2413,22 @@ packages:
       typescript:
         optional: true
     dependencies:
-      '@eslint-sets/eslint-config-basic': 5.12.0(@babel/core@7.23.2)(@typescript-eslint/parser@6.21.0)(eslint@8.57.0)(prettier@3.2.5)(typescript@5.4.5)
-      '@eslint-sets/eslint-config-ts': 5.12.0(@babel/core@7.23.2)(eslint@8.57.0)(prettier@3.2.5)(typescript@5.4.5)
-      '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0)(eslint@8.57.0)(typescript@5.4.5)
-      '@typescript-eslint/parser': 6.21.0(eslint@8.57.0)(typescript@5.4.5)
-      eslint: 8.57.0
-      eslint-config-prettier: 9.1.0(eslint@8.57.0)
-      eslint-plugin-jsdoc: 48.0.6(eslint@8.57.0)
-      eslint-plugin-prettier: 5.1.3(eslint-config-prettier@9.1.0)(eslint@8.57.0)(prettier@3.2.5)
+      '@eslint-sets/eslint-config-basic': 5.12.0(@babel/core@7.24.4)(@typescript-eslint/parser@6.21.0)(eslint@9.0.0)(prettier@3.2.5)(typescript@5.4.5)
+      '@eslint-sets/eslint-config-ts': 5.12.0(@babel/core@7.24.4)(eslint@9.0.0)(prettier@3.2.5)(typescript@5.4.5)
+      '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0)(eslint@9.0.0)(typescript@5.4.5)
+      '@typescript-eslint/parser': 6.21.0(eslint@9.0.0)(typescript@5.4.5)
+      eslint: 9.0.0
+      eslint-config-prettier: 9.1.0(eslint@9.0.0)
+      eslint-plugin-jsdoc: 48.0.6(eslint@9.0.0)
+      eslint-plugin-prettier: 5.1.3(eslint-config-prettier@9.1.0)(eslint@9.0.0)(prettier@3.2.5)
       eslint-plugin-tsdoc: 0.2.17
       eslint-plugin-vitest-globals: 1.4.0
-      eslint-plugin-vue: 9.21.1(eslint@8.57.0)
-      eslint-plugin-vue-scoped-css: 2.7.2(eslint@8.57.0)(vue-eslint-parser@9.4.2)
+      eslint-plugin-vue: 9.21.1(eslint@9.0.0)
+      eslint-plugin-vue-scoped-css: 2.7.2(eslint@9.0.0)(vue-eslint-parser@9.4.2)
       local-pkg: 0.5.0
       prettier: 3.2.5
       typescript: 5.4.5
-      vue-eslint-parser: 9.4.2(eslint@8.57.0)
+      vue-eslint-parser: 9.4.2(eslint@9.0.0)
     transitivePeerDependencies:
       - '@babel/core'
       - '@types/eslint'
@@ -2303,23 +2454,6 @@ packages:
       - supports-color
     dev: true
 
-  /@eslint/eslintrc@2.1.4:
-    resolution: {integrity: sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==}
-    engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
-    dependencies:
-      ajv: 6.12.6
-      debug: 4.3.4(supports-color@8.1.1)
-      espree: 9.6.1
-      globals: 13.24.0
-      ignore: 5.2.4
-      import-fresh: 3.3.0
-      js-yaml: 4.1.0
-      minimatch: 3.1.2
-      strip-json-comments: 3.1.1
-    transitivePeerDependencies:
-      - supports-color
-    dev: true
-
   /@eslint/eslintrc@3.0.2:
     resolution: {integrity: sha512-wV19ZEGEMAC1eHgrS7UQPqsdEiCIbTKTasEfcXAigzoXICcqZSjBZEHlZwNVvKg6UBCjSlos84XiLqsRJnIcIg==}
     engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
@@ -2342,11 +2476,6 @@ packages:
     engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
     dev: true
 
-  /@eslint/js@8.57.0:
-    resolution: {integrity: sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==}
-    engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
-    dev: true
-
   /@eslint/js@9.0.0:
     resolution: {integrity: sha512-RThY/MnKrhubF6+s1JflwUjPEsnCEmYCWwqa/aRISKWNXGZ9epUwft4bUMM35SdKF9xvBrLydAM1RDHd1Z//ZQ==}
     engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
@@ -2363,17 +2492,6 @@ packages:
       - supports-color
     dev: true
 
-  /@humanwhocodes/config-array@0.11.14:
-    resolution: {integrity: sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==}
-    engines: {node: '>=10.10.0'}
-    dependencies:
-      '@humanwhocodes/object-schema': 2.0.3
-      debug: 4.3.4(supports-color@8.1.1)
-      minimatch: 3.1.2
-    transitivePeerDependencies:
-      - supports-color
-    dev: true
-
   /@humanwhocodes/config-array@0.12.3:
     resolution: {integrity: sha512-jsNnTBlMWuTpDkeE3on7+dWJi0D6fdDfeANj/w7MpS8ztROCoLvIO2nG0CcFj+E4k8j4QrSTh4Oryi3i2G669g==}
     engines: {node: '>=10.10.0'}
@@ -2576,7 +2694,7 @@ packages:
     cpu: [wasm32]
     requiresBuild: true
     dependencies:
-      '@emnapi/runtime': 1.1.0
+      '@emnapi/runtime': 1.1.1
     dev: false
     optional: true
 
@@ -2852,14 +2970,33 @@ packages:
       '@jridgewell/sourcemap-codec': 1.4.15
       '@jridgewell/trace-mapping': 0.3.20
 
+  /@jridgewell/gen-mapping@0.3.5:
+    resolution: {integrity: sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==}
+    engines: {node: '>=6.0.0'}
+    dependencies:
+      '@jridgewell/set-array': 1.2.1
+      '@jridgewell/sourcemap-codec': 1.4.15
+      '@jridgewell/trace-mapping': 0.3.25
+    dev: true
+
   /@jridgewell/resolve-uri@3.1.1:
     resolution: {integrity: sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==}
     engines: {node: '>=6.0.0'}
 
+  /@jridgewell/resolve-uri@3.1.2:
+    resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==}
+    engines: {node: '>=6.0.0'}
+    dev: true
+
   /@jridgewell/set-array@1.1.2:
     resolution: {integrity: sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==}
     engines: {node: '>=6.0.0'}
 
+  /@jridgewell/set-array@1.2.1:
+    resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==}
+    engines: {node: '>=6.0.0'}
+    dev: true
+
   /@jridgewell/source-map@0.3.5:
     resolution: {integrity: sha512-UTYAUj/wviwdsMfzoSJspJxbkH5o1snzwX0//0ENX1u/55kkZZkcTZP6u9bwKGkv+dkk9at4m1Cpt0uY80kcpQ==}
     dependencies:
@@ -2881,6 +3018,13 @@ packages:
       '@jridgewell/resolve-uri': 3.1.1
       '@jridgewell/sourcemap-codec': 1.4.15
 
+  /@jridgewell/trace-mapping@0.3.25:
+    resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==}
+    dependencies:
+      '@jridgewell/resolve-uri': 3.1.2
+      '@jridgewell/sourcemap-codec': 1.4.15
+    dev: true
+
   /@jridgewell/trace-mapping@0.3.9:
     resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==}
     dependencies:
@@ -3332,7 +3476,7 @@ packages:
       - encoding
     dev: false
 
-  /@rollup/plugin-alias@5.1.0(rollup@4.14.1):
+  /@rollup/plugin-alias@5.1.0(rollup@4.14.2):
     resolution: {integrity: sha512-lpA3RZ9PdIG7qqhEfv79tBffNaoDuukFDrmhLqg9ifv99u/ehn+lOg30x2zmhf8AQqQUZaMk/B9fZraQ6/acDQ==}
     engines: {node: '>=14.0.0'}
     peerDependencies:
@@ -3341,11 +3485,11 @@ packages:
       rollup:
         optional: true
     dependencies:
-      rollup: 4.14.1
+      rollup: 4.14.2
       slash: 4.0.0
     dev: true
 
-  /@rollup/plugin-json@6.1.0(rollup@4.14.1):
+  /@rollup/plugin-json@6.1.0(rollup@4.14.2):
     resolution: {integrity: sha512-EGI2te5ENk1coGeADSIwZ7G2Q8CJS2sF120T7jLw4xFw9n7wIOXHo+kIYRAoVpJAN+kmqZSoO3Fp4JtoNF4ReA==}
     engines: {node: '>=14.0.0'}
     peerDependencies:
@@ -3354,8 +3498,8 @@ packages:
       rollup:
         optional: true
     dependencies:
-      '@rollup/pluginutils': 5.1.0(rollup@4.14.1)
-      rollup: 4.14.1
+      '@rollup/pluginutils': 5.1.0(rollup@4.14.2)
+      rollup: 4.14.2
     dev: true
 
   /@rollup/pluginutils@4.2.1:
@@ -3366,7 +3510,7 @@ packages:
       picomatch: 2.3.1
     dev: true
 
-  /@rollup/pluginutils@5.1.0(rollup@4.14.1):
+  /@rollup/pluginutils@5.1.0(rollup@4.14.2):
     resolution: {integrity: sha512-XTIWOPPcpvyKI6L1NHo0lFlCyznUEyPmPY1mc3KpPVDYulHSTvyeLNVW00QTLIAFNhR3kYnJTQHeGqU4M3n09g==}
     engines: {node: '>=14.0.0'}
     peerDependencies:
@@ -3378,123 +3522,123 @@ packages:
       '@types/estree': 1.0.3
       estree-walker: 2.0.2
       picomatch: 2.3.1
-      rollup: 4.14.1
+      rollup: 4.14.2
     dev: true
 
-  /@rollup/rollup-android-arm-eabi@4.14.1:
-    resolution: {integrity: sha512-fH8/o8nSUek8ceQnT7K4EQbSiV7jgkHq81m9lWZFIXjJ7lJzpWXbQFpT/Zh6OZYnpFykvzC3fbEvEAFZu03dPA==}
+  /@rollup/rollup-android-arm-eabi@4.14.2:
+    resolution: {integrity: sha512-ahxSgCkAEk+P/AVO0vYr7DxOD3CwAQrT0Go9BJyGQ9Ef0QxVOfjDZMiF4Y2s3mLyPrjonchIMH/tbWHucJMykQ==}
     cpu: [arm]
     os: [android]
     requiresBuild: true
     dev: true
     optional: true
 
-  /@rollup/rollup-android-arm64@4.14.1:
-    resolution: {integrity: sha512-Y/9OHLjzkunF+KGEoJr3heiD5X9OLa8sbT1lm0NYeKyaM3oMhhQFvPB0bNZYJwlq93j8Z6wSxh9+cyKQaxS7PQ==}
+  /@rollup/rollup-android-arm64@4.14.2:
+    resolution: {integrity: sha512-lAarIdxZWbFSHFSDao9+I/F5jDaKyCqAPMq5HqnfpBw8dKDiCaaqM0lq5h1pQTLeIqueeay4PieGR5jGZMWprw==}
     cpu: [arm64]
     os: [android]
     requiresBuild: true
     dev: true
     optional: true
 
-  /@rollup/rollup-darwin-arm64@4.14.1:
-    resolution: {integrity: sha512-+kecg3FY84WadgcuSVm6llrABOdQAEbNdnpi5X3UwWiFVhZIZvKgGrF7kmLguvxHNQy+UuRV66cLVl3S+Rkt+Q==}
+  /@rollup/rollup-darwin-arm64@4.14.2:
+    resolution: {integrity: sha512-SWsr8zEUk82KSqquIMgZEg2GE5mCSfr9sE/thDROkX6pb3QQWPp8Vw8zOq2GyxZ2t0XoSIUlvHDkrf5Gmf7x3Q==}
     cpu: [arm64]
     os: [darwin]
     requiresBuild: true
     dev: true
     optional: true
 
-  /@rollup/rollup-darwin-x64@4.14.1:
-    resolution: {integrity: sha512-2pYRzEjVqq2TB/UNv47BV/8vQiXkFGVmPFwJb+1E0IFFZbIX8/jo1olxqqMbo6xCXf8kabANhp5bzCij2tFLUA==}
+  /@rollup/rollup-darwin-x64@4.14.2:
+    resolution: {integrity: sha512-o/HAIrQq0jIxJAhgtIvV5FWviYK4WB0WwV91SLUnsliw1lSAoLsmgEEgRWzDguAFeUEUUoIWXiJrPqU7vGiVkA==}
     cpu: [x64]
     os: [darwin]
     requiresBuild: true
     dev: true
     optional: true
 
-  /@rollup/rollup-linux-arm-gnueabihf@4.14.1:
-    resolution: {integrity: sha512-mS6wQ6Do6/wmrF9aTFVpIJ3/IDXhg1EZcQFYHZLHqw6AzMBjTHWnCG35HxSqUNphh0EHqSM6wRTT8HsL1C0x5g==}
+  /@rollup/rollup-linux-arm-gnueabihf@4.14.2:
+    resolution: {integrity: sha512-nwlJ65UY9eGq91cBi6VyDfArUJSKOYt5dJQBq8xyLhvS23qO+4Nr/RreibFHjP6t+5ap2ohZrUJcHv5zk5ju/g==}
     cpu: [arm]
     os: [linux]
     requiresBuild: true
     dev: true
     optional: true
 
-  /@rollup/rollup-linux-arm64-gnu@4.14.1:
-    resolution: {integrity: sha512-p9rGKYkHdFMzhckOTFubfxgyIO1vw//7IIjBBRVzyZebWlzRLeNhqxuSaZ7kCEKVkm/kuC9fVRW9HkC/zNRG2w==}
+  /@rollup/rollup-linux-arm64-gnu@4.14.2:
+    resolution: {integrity: sha512-Pg5TxxO2IVlMj79+c/9G0LREC9SY3HM+pfAwX7zj5/cAuwrbfj2Wv9JbMHIdPCfQpYsI4g9mE+2Bw/3aeSs2rQ==}
     cpu: [arm64]
     os: [linux]
     requiresBuild: true
     dev: true
     optional: true
 
-  /@rollup/rollup-linux-arm64-musl@4.14.1:
-    resolution: {integrity: sha512-nDY6Yz5xS/Y4M2i9JLQd3Rofh5OR8Bn8qe3Mv/qCVpHFlwtZSBYSPaU4mrGazWkXrdQ98GB//H0BirGR/SKFSw==}
+  /@rollup/rollup-linux-arm64-musl@4.14.2:
+    resolution: {integrity: sha512-cAOTjGNm84gc6tS02D1EXtG7tDRsVSDTBVXOLbj31DkwfZwgTPYZ6aafSU7rD/4R2a34JOwlF9fQayuTSkoclA==}
     cpu: [arm64]
     os: [linux]
     requiresBuild: true
     dev: true
     optional: true
 
-  /@rollup/rollup-linux-powerpc64le-gnu@4.14.1:
-    resolution: {integrity: sha512-im7HE4VBL+aDswvcmfx88Mp1soqL9OBsdDBU8NqDEYtkri0qV0THhQsvZtZeNNlLeCUQ16PZyv7cqutjDF35qw==}
-    cpu: [ppc64le]
+  /@rollup/rollup-linux-powerpc64le-gnu@4.14.2:
+    resolution: {integrity: sha512-4RyT6v1kXb7C0fn6zV33rvaX05P0zHoNzaXI/5oFHklfKm602j+N4mn2YvoezQViRLPnxP8M1NaY4s/5kXO5cw==}
+    cpu: [ppc64]
     os: [linux]
     requiresBuild: true
     dev: true
     optional: true
 
-  /@rollup/rollup-linux-riscv64-gnu@4.14.1:
-    resolution: {integrity: sha512-RWdiHuAxWmzPJgaHJdpvUUlDz8sdQz4P2uv367T2JocdDa98iRw2UjIJ4QxSyt077mXZT2X6pKfT2iYtVEvOFw==}
+  /@rollup/rollup-linux-riscv64-gnu@4.14.2:
+    resolution: {integrity: sha512-KNUH6jC/vRGAKSorySTyc/yRYlCwN/5pnMjXylfBniwtJx5O7X17KG/0efj8XM3TZU7raYRXJFFReOzNmL1n1w==}
     cpu: [riscv64]
     os: [linux]
     requiresBuild: true
     dev: true
     optional: true
 
-  /@rollup/rollup-linux-s390x-gnu@4.14.1:
-    resolution: {integrity: sha512-VMgaGQ5zRX6ZqV/fas65/sUGc9cPmsntq2FiGmayW9KMNfWVG/j0BAqImvU4KTeOOgYSf1F+k6at1UfNONuNjA==}
+  /@rollup/rollup-linux-s390x-gnu@4.14.2:
+    resolution: {integrity: sha512-xPV4y73IBEXToNPa3h5lbgXOi/v0NcvKxU0xejiFw6DtIYQqOTMhZ2DN18/HrrP0PmiL3rGtRG9gz1QE8vFKXQ==}
     cpu: [s390x]
     os: [linux]
     requiresBuild: true
     dev: true
     optional: true
 
-  /@rollup/rollup-linux-x64-gnu@4.14.1:
-    resolution: {integrity: sha512-9Q7DGjZN+hTdJomaQ3Iub4m6VPu1r94bmK2z3UeWP3dGUecRC54tmVu9vKHTm1bOt3ASoYtEz6JSRLFzrysKlA==}
+  /@rollup/rollup-linux-x64-gnu@4.14.2:
+    resolution: {integrity: sha512-QBhtr07iFGmF9egrPOWyO5wciwgtzKkYPNLVCFZTmr4TWmY0oY2Dm/bmhHjKRwZoGiaKdNcKhFtUMBKvlchH+Q==}
     cpu: [x64]
     os: [linux]
     requiresBuild: true
     dev: true
     optional: true
 
-  /@rollup/rollup-linux-x64-musl@4.14.1:
-    resolution: {integrity: sha512-JNEG/Ti55413SsreTguSx0LOVKX902OfXIKVg+TCXO6Gjans/k9O6ww9q3oLGjNDaTLxM+IHFMeXy/0RXL5R/g==}
+  /@rollup/rollup-linux-x64-musl@4.14.2:
+    resolution: {integrity: sha512-8zfsQRQGH23O6qazZSFY5jP5gt4cFvRuKTpuBsC1ZnSWxV8ZKQpPqOZIUtdfMOugCcBvFGRa1pDC/tkf19EgBw==}
     cpu: [x64]
     os: [linux]
     requiresBuild: true
     dev: true
     optional: true
 
-  /@rollup/rollup-win32-arm64-msvc@4.14.1:
-    resolution: {integrity: sha512-ryS22I9y0mumlLNwDFYZRDFLwWh3aKaC72CWjFcFvxK0U6v/mOkM5Up1bTbCRAhv3kEIwW2ajROegCIQViUCeA==}
+  /@rollup/rollup-win32-arm64-msvc@4.14.2:
+    resolution: {integrity: sha512-H4s8UjgkPnlChl6JF5empNvFHp77Jx+Wfy2EtmYPe9G22XV+PMuCinZVHurNe8ggtwoaohxARJZbaH/3xjB/FA==}
     cpu: [arm64]
     os: [win32]
     requiresBuild: true
     dev: true
     optional: true
 
-  /@rollup/rollup-win32-ia32-msvc@4.14.1:
-    resolution: {integrity: sha512-TdloItiGk+T0mTxKx7Hp279xy30LspMso+GzQvV2maYePMAWdmrzqSNZhUpPj3CGw12aGj57I026PgLCTu8CGg==}
+  /@rollup/rollup-win32-ia32-msvc@4.14.2:
+    resolution: {integrity: sha512-djqpAjm/i8erWYF0K6UY4kRO3X5+T4TypIqw60Q8MTqSBaQNpNXDhxdjpZ3ikgb+wn99svA7jxcXpiyg9MUsdw==}
     cpu: [ia32]
     os: [win32]
     requiresBuild: true
     dev: true
     optional: true
 
-  /@rollup/rollup-win32-x64-msvc@4.14.1:
-    resolution: {integrity: sha512-wQGI+LY/Py20zdUPq+XCem7JcPOyzIJBm3dli+56DJsQOHbnXZFEwgmnC6el1TPAfC8lBT3m+z69RmLykNUbew==}
+  /@rollup/rollup-win32-x64-msvc@4.14.2:
+    resolution: {integrity: sha512-teAqzLT0yTYZa8ZP7zhFKEx4cotS8Tkk5XiqNMJhD4CpaWB1BHARE4Qy+RzwnXvSAYv+Q3jAqCVBS+PS+Yee8Q==}
     cpu: [x64]
     os: [win32]
     requiresBuild: true
@@ -4301,8 +4445,8 @@ packages:
     resolution: {integrity: sha512-mQkU2jY8jJEF7YHjHvsQO8+3ughTL1mcnn96igfhONmR+fUPSKIkefQYpSe8bsly2Ep7oQbn/6VG5/9/0qcArQ==}
     dev: true
 
-  /@types/sortablejs@1.15.4:
-    resolution: {integrity: sha512-7oL7CcPSfoyoNx3Ba1+79ykJzpEKVhHUyfAiN5eT/FoeDXOR3eBDLXf9ndDNuxaExmjpI+zVi2dMMuaoXUOzNA==}
+  /@types/sortablejs@1.15.8:
+    resolution: {integrity: sha512-b79830lW+RZfwaztgs1aVPgbasJ8e7AXtZYHTELNXZPsERt4ymJdjV4OccDbHQAvHrCcFpbF78jkm0R6h/pZVg==}
     dev: true
 
   /@types/stack-utils@2.0.2:
@@ -4435,7 +4579,7 @@ packages:
       - supports-color
     dev: true
 
-  /@typescript-eslint/eslint-plugin@5.62.0(@typescript-eslint/parser@5.62.0)(eslint@8.57.0)(typescript@5.4.5):
+  /@typescript-eslint/eslint-plugin@5.62.0(@typescript-eslint/parser@5.62.0)(eslint@9.0.0)(typescript@5.4.5):
     resolution: {integrity: sha512-TiZzBSJja/LbhNPvk6yc0JrX9XqhQ0hdh6M2svYfsHGejaKFIAGd9MQ+ERIMzLGlN/kZoYIgdxFV0PuljTKXag==}
     engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
     peerDependencies:
@@ -4447,12 +4591,12 @@ packages:
         optional: true
     dependencies:
       '@eslint-community/regexpp': 4.9.1
-      '@typescript-eslint/parser': 5.62.0(eslint@8.57.0)(typescript@5.4.5)
+      '@typescript-eslint/parser': 5.62.0(eslint@9.0.0)(typescript@5.4.5)
       '@typescript-eslint/scope-manager': 5.62.0
-      '@typescript-eslint/type-utils': 5.62.0(eslint@8.57.0)(typescript@5.4.5)
-      '@typescript-eslint/utils': 5.62.0(eslint@8.57.0)(typescript@5.4.5)
+      '@typescript-eslint/type-utils': 5.62.0(eslint@9.0.0)(typescript@5.4.5)
+      '@typescript-eslint/utils': 5.62.0(eslint@9.0.0)(typescript@5.4.5)
       debug: 4.3.4(supports-color@8.1.1)
-      eslint: 8.57.0
+      eslint: 9.0.0
       graphemer: 1.4.0
       ignore: 5.2.4
       natural-compare-lite: 1.4.0
@@ -4463,7 +4607,7 @@ packages:
       - supports-color
     dev: true
 
-  /@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0)(eslint@8.57.0)(typescript@5.4.5):
+  /@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0)(eslint@9.0.0)(typescript@5.4.5):
     resolution: {integrity: sha512-oy9+hTPCUFpngkEZUSzbf9MxI65wbKFoQYsgPdILTfbUldp5ovUuphZVe4i30emU9M/kP+T64Di0mxl7dSw3MA==}
     engines: {node: ^16.0.0 || >=18.0.0}
     peerDependencies:
@@ -4475,13 +4619,13 @@ packages:
         optional: true
     dependencies:
       '@eslint-community/regexpp': 4.9.1
-      '@typescript-eslint/parser': 6.21.0(eslint@8.57.0)(typescript@5.4.5)
+      '@typescript-eslint/parser': 6.21.0(eslint@9.0.0)(typescript@5.4.5)
       '@typescript-eslint/scope-manager': 6.21.0
-      '@typescript-eslint/type-utils': 6.21.0(eslint@8.57.0)(typescript@5.4.5)
-      '@typescript-eslint/utils': 6.21.0(eslint@8.57.0)(typescript@5.4.5)
+      '@typescript-eslint/type-utils': 6.21.0(eslint@9.0.0)(typescript@5.4.5)
+      '@typescript-eslint/utils': 6.21.0(eslint@9.0.0)(typescript@5.4.5)
       '@typescript-eslint/visitor-keys': 6.21.0
       debug: 4.3.4(supports-color@8.1.1)
-      eslint: 8.57.0
+      eslint: 9.0.0
       graphemer: 1.4.0
       ignore: 5.2.4
       natural-compare: 1.4.0
@@ -4512,7 +4656,7 @@ packages:
       - supports-color
     dev: true
 
-  /@typescript-eslint/parser@5.62.0(eslint@8.57.0)(typescript@5.4.5):
+  /@typescript-eslint/parser@5.62.0(eslint@9.0.0)(typescript@5.4.5):
     resolution: {integrity: sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==}
     engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
     peerDependencies:
@@ -4526,13 +4670,13 @@ packages:
       '@typescript-eslint/types': 5.62.0
       '@typescript-eslint/typescript-estree': 5.62.0(typescript@5.4.5)
       debug: 4.3.4(supports-color@8.1.1)
-      eslint: 8.57.0
+      eslint: 9.0.0
       typescript: 5.4.5
     transitivePeerDependencies:
       - supports-color
     dev: true
 
-  /@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.4.5):
+  /@typescript-eslint/parser@6.21.0(eslint@9.0.0)(typescript@5.4.5):
     resolution: {integrity: sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==}
     engines: {node: ^16.0.0 || >=18.0.0}
     peerDependencies:
@@ -4547,7 +4691,7 @@ packages:
       '@typescript-eslint/typescript-estree': 6.21.0(typescript@5.4.5)
       '@typescript-eslint/visitor-keys': 6.21.0
       debug: 4.3.4(supports-color@8.1.1)
-      eslint: 8.57.0
+      eslint: 9.0.0
       typescript: 5.4.5
     transitivePeerDependencies:
       - supports-color
@@ -4589,7 +4733,7 @@ packages:
       - supports-color
     dev: true
 
-  /@typescript-eslint/type-utils@5.62.0(eslint@8.57.0)(typescript@5.4.5):
+  /@typescript-eslint/type-utils@5.62.0(eslint@9.0.0)(typescript@5.4.5):
     resolution: {integrity: sha512-xsSQreu+VnfbqQpW5vnCJdq1Z3Q0U31qiWmRhr98ONQmcp/yhiPJFPq8MXiJVLiksmOKSjIldZzkebzHuCGzew==}
     engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
     peerDependencies:
@@ -4600,16 +4744,16 @@ packages:
         optional: true
     dependencies:
       '@typescript-eslint/typescript-estree': 5.62.0(typescript@5.4.5)
-      '@typescript-eslint/utils': 5.62.0(eslint@8.57.0)(typescript@5.4.5)
+      '@typescript-eslint/utils': 5.62.0(eslint@9.0.0)(typescript@5.4.5)
       debug: 4.3.4(supports-color@8.1.1)
-      eslint: 8.57.0
+      eslint: 9.0.0
       tsutils: 3.21.0(typescript@5.4.5)
       typescript: 5.4.5
     transitivePeerDependencies:
       - supports-color
     dev: true
 
-  /@typescript-eslint/type-utils@6.21.0(eslint@8.57.0)(typescript@5.4.5):
+  /@typescript-eslint/type-utils@6.21.0(eslint@9.0.0)(typescript@5.4.5):
     resolution: {integrity: sha512-rZQI7wHfao8qMX3Rd3xqeYSMCL3SoiSQLBATSiVKARdFGCYSRvmViieZjqc58jKgs8Y8i9YvVVhRbHSTA4VBag==}
     engines: {node: ^16.0.0 || >=18.0.0}
     peerDependencies:
@@ -4620,9 +4764,9 @@ packages:
         optional: true
     dependencies:
       '@typescript-eslint/typescript-estree': 6.21.0(typescript@5.4.5)
-      '@typescript-eslint/utils': 6.21.0(eslint@8.57.0)(typescript@5.4.5)
+      '@typescript-eslint/utils': 6.21.0(eslint@9.0.0)(typescript@5.4.5)
       debug: 4.3.4(supports-color@8.1.1)
-      eslint: 8.57.0
+      eslint: 9.0.0
       ts-api-utils: 1.0.1(typescript@5.4.5)
       typescript: 5.4.5
     transitivePeerDependencies:
@@ -4723,19 +4867,19 @@ packages:
       - typescript
     dev: true
 
-  /@typescript-eslint/utils@5.62.0(eslint@8.57.0)(typescript@5.4.5):
+  /@typescript-eslint/utils@5.62.0(eslint@9.0.0)(typescript@5.4.5):
     resolution: {integrity: sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ==}
     engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
     peerDependencies:
       eslint: ^6.0.0 || ^7.0.0 || ^8.0.0
     dependencies:
-      '@eslint-community/eslint-utils': 4.4.0(eslint@8.57.0)
+      '@eslint-community/eslint-utils': 4.4.0(eslint@9.0.0)
       '@types/json-schema': 7.0.12
       '@types/semver': 7.5.8
       '@typescript-eslint/scope-manager': 5.62.0
       '@typescript-eslint/types': 5.62.0
       '@typescript-eslint/typescript-estree': 5.62.0(typescript@5.4.5)
-      eslint: 8.57.0
+      eslint: 9.0.0
       eslint-scope: 5.1.1
       semver: 7.6.0
     transitivePeerDependencies:
@@ -4743,19 +4887,19 @@ packages:
       - typescript
     dev: true
 
-  /@typescript-eslint/utils@6.21.0(eslint@8.57.0)(typescript@5.4.5):
+  /@typescript-eslint/utils@6.21.0(eslint@9.0.0)(typescript@5.4.5):
     resolution: {integrity: sha512-NfWVaC8HP9T8cbKQxHcsJBY5YE1O33+jpMwN45qzWWaPDZgLIbo12toGMWnmhvCpd3sIxkpDw3Wv1B3dYrbDQQ==}
     engines: {node: ^16.0.0 || >=18.0.0}
     peerDependencies:
       eslint: ^7.0.0 || ^8.0.0
     dependencies:
-      '@eslint-community/eslint-utils': 4.4.0(eslint@8.57.0)
+      '@eslint-community/eslint-utils': 4.4.0(eslint@9.0.0)
       '@types/json-schema': 7.0.14
       '@types/semver': 7.5.8
       '@typescript-eslint/scope-manager': 6.21.0
       '@typescript-eslint/types': 6.21.0
       '@typescript-eslint/typescript-estree': 6.21.0(typescript@5.4.5)
-      eslint: 8.57.0
+      eslint: 9.0.0
       semver: 7.6.0
     transitivePeerDependencies:
       - supports-color
@@ -4778,10 +4922,6 @@ packages:
       eslint-visitor-keys: 3.4.3
     dev: true
 
-  /@ungap/structured-clone@1.2.0:
-    resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==}
-    dev: true
-
   /@vercel/nft@0.26.3:
     resolution: {integrity: sha512-h1z/NN9ppS4YOKwSgBoopJlhm7tS2Qb/9Ld1HXjDpvvTE7mY0xVD8nllXs+RihD9uTGJISOIMzp18Eg0EApaMA==}
     engines: {node: '>=16'}
@@ -4811,26 +4951,26 @@ packages:
       vite: ^5.0.0
       vue: ^3.2.25
     dependencies:
-      vite: 5.2.8(@types/node@20.12.7)(sass@1.74.1)
+      vite: 5.2.8(@types/node@20.12.7)(sass@1.75.0)
       vue: 3.4.21(typescript@5.4.5)
     dev: true
 
-  /@volar/language-core@2.2.0-alpha.7:
-    resolution: {integrity: sha512-igpp+nTkyl8faVzRJMpSCeA4XlBJ5UVSyc/WGyksmUmP10YbfufbcQCFlxEXv2uMBV+a3L4JVCj+Vju+08FOSA==}
+  /@volar/language-core@2.2.0-alpha.8:
+    resolution: {integrity: sha512-Ew1Iw7/RIRNuDLn60fWJdOLApAlfTVPxbPiSLzc434PReC9kleYtaa//Wo2WlN1oiRqneW0pWQQV0CwYqaimLQ==}
     dependencies:
-      '@volar/source-map': 2.2.0-alpha.7
+      '@volar/source-map': 2.2.0-alpha.8
     dev: true
 
-  /@volar/source-map@2.2.0-alpha.7:
-    resolution: {integrity: sha512-iIZM2EovdEnr6mMwlsnt4ciix4xz7HSGHyUSviRaY5cii5PMXGHeUU9UDeb+xzLCx8kdk3L5J4z+ts50AhkYcg==}
+  /@volar/source-map@2.2.0-alpha.8:
+    resolution: {integrity: sha512-E1ZVmXFJ5DU4fWDcWHzi8OLqqReqIDwhXvIMhVdk6+VipfMVv4SkryXu7/rs4GA/GsebcRyJdaSkKBB3OAkIcA==}
     dependencies:
       muggle-string: 0.4.1
     dev: true
 
-  /@volar/typescript@2.2.0-alpha.7:
-    resolution: {integrity: sha512-qy04/hx4UbW1BdPlzaxlH60D4plubcyqdbYM6Y5vZiascZxFowtd6vE39Td9FYzDxwcKgzb/Crvf/ABhdHnuBA==}
+  /@volar/typescript@2.2.0-alpha.8:
+    resolution: {integrity: sha512-RLbRDI+17CiayHZs9HhSzlH0FhLl/+XK6o2qoiw2o2GGKcyD1aDoY6AcMd44acYncTOrqoTNoY6LuCiRyiJiGg==}
     dependencies:
-      '@volar/language-core': 2.2.0-alpha.7
+      '@volar/language-core': 2.2.0-alpha.8
       path-browserify: 1.0.1
     dev: true
 
@@ -4880,19 +5020,19 @@ packages:
       '@vue/shared': 3.4.21
     dev: true
 
-  /@vue/language-core@2.0.12(typescript@5.4.5):
-    resolution: {integrity: sha512-aIStDPt69SHOpiIckGTIIjEz/sXc6ZfCMS5uWYL1AcbcRMhzFCLZscGAVte1+ad+RRFepSpKBjGttyPcgKJ7ww==}
+  /@vue/language-core@2.0.13(typescript@5.4.5):
+    resolution: {integrity: sha512-oQgM+BM66SU5GKtUMLQSQN0bxHFkFpLSSAiY87wVziPaiNQZuKVDt/3yA7GB9PiQw0y/bTNL0bOc0jM/siYjKg==}
     peerDependencies:
       typescript: '*'
     peerDependenciesMeta:
       typescript:
         optional: true
     dependencies:
-      '@volar/language-core': 2.2.0-alpha.7
+      '@volar/language-core': 2.2.0-alpha.8
       '@vue/compiler-dom': 3.4.21
       '@vue/shared': 3.4.21
       computeds: 0.0.1
-      minimatch: 9.0.3
+      minimatch: 9.0.4
       path-browserify: 1.0.1
       typescript: 5.4.5
       vue-template-compiler: 2.7.16
@@ -5607,6 +5747,10 @@ packages:
     resolution: {integrity: sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==}
     dev: false
 
+  /async@3.2.5:
+    resolution: {integrity: sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==}
+    dev: false
+
   /asynckit@0.4.0:
     resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
 
@@ -5693,9 +5837,17 @@ packages:
   /available-typed-arrays@1.0.5:
     resolution: {integrity: sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==}
     engines: {node: '>= 0.4'}
+    dev: true
 
-  /aws-sdk@2.1597.0:
-    resolution: {integrity: sha512-YvApP9p5a5TD870mvQRrcUyJz3nKFrtlnDLaA4yrmAaidMDGzdNJ+AZlW0+onRCB4llzKD4Hos56zea0ulR+zQ==}
+  /available-typed-arrays@1.0.7:
+    resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==}
+    engines: {node: '>= 0.4'}
+    dependencies:
+      possible-typed-array-names: 1.0.0
+    dev: false
+
+  /aws-sdk@2.1599.0:
+    resolution: {integrity: sha512-jPb1LAN+s1TLTK+VR3TTJLr//sb3AhhT60Bm9jxB5G/fVeeRczXtBtixNpQ00gksQdkstILYLc9S6MuKMsksxA==}
     engines: {node: '>= 10.0.0'}
     requiresBuild: true
     dependencies:
@@ -6022,6 +6174,17 @@ packages:
       node-releases: 2.0.13
       update-browserslist-db: 1.0.13(browserslist@4.22.1)
 
+  /browserslist@4.23.0:
+    resolution: {integrity: sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ==}
+    engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
+    hasBin: true
+    dependencies:
+      caniuse-lite: 1.0.30001609
+      electron-to-chromium: 1.4.736
+      node-releases: 2.0.14
+      update-browserslist-db: 1.0.13(browserslist@4.23.0)
+    dev: true
+
   /bs-logger@0.2.6:
     resolution: {integrity: sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==}
     engines: {node: '>= 6'}
@@ -6075,7 +6238,7 @@ packages:
     resolution: {integrity: sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg==}
     dependencies:
       base64-js: 1.5.1
-      ieee754: 1.2.1
+      ieee754: 1.1.13
       isarray: 1.0.0
     dev: false
 
@@ -6260,6 +6423,10 @@ packages:
   /caniuse-lite@1.0.30001551:
     resolution: {integrity: sha512-vtBAez47BoGMMzlbYhfXrMV1kvRF2WP/lqiMuDu1Sb4EE4LKEgjopFDSRtZfdVnslNRpOqV/woE+Xgrwj6VQlg==}
 
+  /caniuse-lite@1.0.30001609:
+    resolution: {integrity: sha512-JFPQs34lHKx1B5t1EpQpWH4c+29zIyn/haGsbpfq3suuV9v56enjFt23zqijxGTMwy1p/4H2tjnQMY+p1WoAyA==}
+    dev: true
+
   /canonicalize@1.0.8:
     resolution: {integrity: sha512-0CNTVCLZggSh7bc5VkX5WWPWO+cyZbNd07IHIsSXLia/eAq+r836hgk+8BKoEh7949Mda87VUOitx5OddVj64A==}
     dev: false
@@ -6451,6 +6618,21 @@ packages:
       fsevents: 2.3.3
     dev: true
 
+  /chokidar@3.6.0:
+    resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==}
+    engines: {node: '>= 8.10.0'}
+    dependencies:
+      anymatch: 3.1.3
+      braces: 3.0.2
+      glob-parent: 5.1.2
+      is-binary-path: 2.1.0
+      is-glob: 4.0.3
+      normalize-path: 3.0.0
+      readdirp: 3.6.0
+    optionalDependencies:
+      fsevents: 2.3.3
+    dev: true
+
   /chownr@2.0.0:
     resolution: {integrity: sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==}
     engines: {node: '>=10'}
@@ -6623,7 +6805,7 @@ packages:
     resolution: {integrity: sha512-sX/LQ7LqUhgyaxzbe7IqwPeTr2yfpfUIQ/dgpKo6ZI4y4lpQA0YxAomWIY+7I7rHWcG02PG+OuPREzMW/5tszQ==}
     dependencies:
       inflation: 2.0.0
-      qs: 6.12.0
+      qs: 6.12.1
       raw-body: 2.5.2
       type-is: 1.6.18
     dev: false
@@ -6632,7 +6814,7 @@ packages:
     resolution: {integrity: sha512-m7pOT6CdLN7FuXUcpuz/8lfQ/L77x8SchHCF4G0RBTJO20Wzmhn5Sp4/5WsKy8OSpifBSUrmg83qEqaDHdyFuQ==}
     dependencies:
       inflation: 2.0.0
-      qs: 6.12.0
+      qs: 6.12.1
       raw-body: 2.5.2
       type-is: 1.6.18
     dev: false
@@ -6845,7 +7027,7 @@ packages:
     resolution: {integrity: sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==}
     dev: true
 
-  /consolidate@0.16.0(ejs@3.1.9)(pug@3.0.2):
+  /consolidate@0.16.0(ejs@3.1.10)(pug@3.0.2):
     resolution: {integrity: sha512-Nhl1wzCslqXYTJVDyJCu3ODohy9OfBMB5uD2BiBTzd7w+QY0lBzafkR8y8755yMYHAaMD4NuzbAw03/xzfw+eQ==}
     engines: {node: '>= 0.10.0'}
     deprecated: Please upgrade to consolidate v1.0.0+ as it has been modernized with several long-awaited fixes implemented. Maintenance is supported by Forward Email at https://forwardemail.net ; follow/watch https://github.com/ladjs/consolidate for updates and release changelog
@@ -7012,7 +7194,7 @@ packages:
         optional: true
     dependencies:
       bluebird: 3.7.2
-      ejs: 3.1.9
+      ejs: 3.1.10
       pug: 3.0.2
     dev: false
 
@@ -7077,8 +7259,8 @@ packages:
     resolution: {integrity: sha512-3DdaFaU/Zf1AnpLiFDeNCD4TOWe3Zl2RZaTzUvWiIk5ERzcCodOE20Vqq4fzCbNoHURFHT4/us/Lfq+S2zyY4w==}
     dev: false
 
-  /core-js@3.33.0:
-    resolution: {integrity: sha512-HoZr92+ZjFEKar5HS6MC776gYslNOKHt75mEBKWKnPeFDpZ6nH5OeF3S6HFT1mUAUZKrzkez05VboaX8myjSuw==}
+  /core-js@3.36.1:
+    resolution: {integrity: sha512-BTvUrwxVBezj5SZ3f10ImnX2oRByMxql3EimVqMysepbC9EeMUOpLwdy6Eoili2x6E4kf+ZUB5k/+Jv55alPfA==}
     requiresBuild: true
     dev: true
 
@@ -7477,8 +7659,8 @@ packages:
     resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==}
     dev: true
 
-  /deepl-node@1.12.0:
-    resolution: {integrity: sha512-c/8x1R0dXPL7NSDdQ94lYPou/A+I6cbo6b7gFb/28HbjcHnKB4RtWXWLgdv7n51GEXL7OE2eoRZQcAu4ZI+vGg==}
+  /deepl-node@1.13.0:
+    resolution: {integrity: sha512-pm8Al5B+/fRHiIKoreoSmv2RlXidF18+CznhtLILiYcj3EbxZpIhxWO8cgXCCsCTrUDMAbScIl8CuH3AqLPpGg==}
     engines: {node: '>=12.0'}
     dependencies:
       '@types/node': 20.12.7
@@ -7763,8 +7945,8 @@ packages:
     resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==}
     dev: false
 
-  /ejs@3.1.9:
-    resolution: {integrity: sha512-rC+QVNMJWv+MtPgkt0y+0rVEIdbtxVADApW9JXrUVlzHetgcyczP/E7DJmWJ4fJCZF2cPcBk0laWO9ZHMG3DmQ==}
+  /ejs@3.1.10:
+    resolution: {integrity: sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==}
     engines: {node: '>=0.10.0'}
     hasBin: true
     dependencies:
@@ -7778,6 +7960,10 @@ packages:
   /electron-to-chromium@1.4.561:
     resolution: {integrity: sha512-eS5t4ulWOBfVHdq9SW2dxEaFarj1lPjvJ8PaYMOjY0DecBaj/t4ARziL2IPpDr4atyWwjLFGQ2vo/VCgQFezVQ==}
 
+  /electron-to-chromium@1.4.736:
+    resolution: {integrity: sha512-Rer6wc3ynLelKNM4lOCg7/zPQj8tPOCB2hzD32PX9wd3hgRRi9MxEbmkFCokzcEhRVMiOVLjnL9ig9cefJ+6+Q==}
+    dev: true
+
   /emittery@0.13.1:
     resolution: {integrity: sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==}
     engines: {node: '>=12'}
@@ -8028,41 +8214,41 @@ packages:
     engines: {node: '>=12'}
     dev: true
 
-  /eslint-compat-utils@0.1.2(eslint@8.57.0):
+  /eslint-compat-utils@0.1.2(eslint@9.0.0):
     resolution: {integrity: sha512-Jia4JDldWnFNIru1Ehx1H5s9/yxiRHY/TimCuUc0jNexew3cF1gI6CYZil1ociakfWO3rRqFjl1mskBblB3RYg==}
     engines: {node: '>=12'}
     peerDependencies:
       eslint: '>=6.0.0'
     dependencies:
-      eslint: 8.57.0
+      eslint: 9.0.0
     dev: true
 
-  /eslint-compat-utils@0.4.1(eslint@8.57.0):
+  /eslint-compat-utils@0.4.1(eslint@9.0.0):
     resolution: {integrity: sha512-5N7ZaJG5pZxUeNNJfUchurLVrunD1xJvyg5kYOIVF8kg1f3ajTikmAu/5fZ9w100omNPOoMjngRszh/Q/uFGMg==}
     engines: {node: '>=12'}
     peerDependencies:
       eslint: '>=6.0.0'
     dependencies:
-      eslint: 8.57.0
+      eslint: 9.0.0
       semver: 7.6.0
     dev: true
 
-  /eslint-config-prettier@8.9.0(eslint@8.57.0):
+  /eslint-config-prettier@8.9.0(eslint@9.0.0):
     resolution: {integrity: sha512-+sbni7NfVXnOpnRadUA8S28AUlsZt9GjgFvABIRL9Hkn8KqNzOp+7Lw4QWtrwn20KzU3wqu1QoOj2m+7rKRqkA==}
     hasBin: true
     peerDependencies:
       eslint: '>=7.0.0'
     dependencies:
-      eslint: 8.57.0
+      eslint: 9.0.0
     dev: true
 
-  /eslint-config-prettier@9.1.0(eslint@8.57.0):
+  /eslint-config-prettier@9.1.0(eslint@9.0.0):
     resolution: {integrity: sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==}
     hasBin: true
     peerDependencies:
       eslint: '>=7.0.0'
     dependencies:
-      eslint: 8.57.0
+      eslint: 9.0.0
     dev: true
 
   /eslint-config-standard@16.0.3(eslint-plugin-import@2.28.0)(eslint-plugin-node@11.1.0)(eslint-plugin-promise@6.1.1)(eslint@8.46.0):
@@ -8132,7 +8318,7 @@ packages:
       - supports-color
     dev: true
 
-  /eslint-module-utils@2.8.0(@typescript-eslint/parser@5.62.0)(eslint-import-resolver-node@0.3.9)(eslint@8.57.0):
+  /eslint-module-utils@2.8.0(@typescript-eslint/parser@5.62.0)(eslint-import-resolver-node@0.3.9)(eslint@9.0.0):
     resolution: {integrity: sha512-aWajIYfsqCKRDgUfjEXNN/JlrzauMuSEy5sbd7WXbtW3EH6A6MpwEh42c7qD+MqQo9QMJ6fWLAeIJynx0g6OAw==}
     engines: {node: '>=4'}
     peerDependencies:
@@ -8153,15 +8339,15 @@ packages:
       eslint-import-resolver-webpack:
         optional: true
     dependencies:
-      '@typescript-eslint/parser': 5.62.0(eslint@8.57.0)(typescript@5.4.5)
+      '@typescript-eslint/parser': 5.62.0(eslint@9.0.0)(typescript@5.4.5)
       debug: 3.2.7
-      eslint: 8.57.0
+      eslint: 9.0.0
       eslint-import-resolver-node: 0.3.9
     transitivePeerDependencies:
       - supports-color
     dev: true
 
-  /eslint-module-utils@2.8.0(@typescript-eslint/parser@6.21.0)(eslint-import-resolver-node@0.3.9)(eslint@8.57.0):
+  /eslint-module-utils@2.8.0(@typescript-eslint/parser@6.21.0)(eslint-import-resolver-node@0.3.9)(eslint@9.0.0):
     resolution: {integrity: sha512-aWajIYfsqCKRDgUfjEXNN/JlrzauMuSEy5sbd7WXbtW3EH6A6MpwEh42c7qD+MqQo9QMJ6fWLAeIJynx0g6OAw==}
     engines: {node: '>=4'}
     peerDependencies:
@@ -8182,24 +8368,24 @@ packages:
       eslint-import-resolver-webpack:
         optional: true
     dependencies:
-      '@typescript-eslint/parser': 6.21.0(eslint@8.57.0)(typescript@5.4.5)
+      '@typescript-eslint/parser': 6.21.0(eslint@9.0.0)(typescript@5.4.5)
       debug: 3.2.7
-      eslint: 8.57.0
+      eslint: 9.0.0
       eslint-import-resolver-node: 0.3.9
     transitivePeerDependencies:
       - supports-color
     dev: true
 
-  /eslint-plugin-es-x@7.5.0(eslint@8.57.0):
+  /eslint-plugin-es-x@7.5.0(eslint@9.0.0):
     resolution: {integrity: sha512-ODswlDSO0HJDzXU0XvgZ3lF3lS3XAZEossh15Q2UHjwrJggWeBoKqqEsLTZLXl+dh5eOAozG0zRcYtuE35oTuQ==}
     engines: {node: ^14.18.0 || >=16.0.0}
     peerDependencies:
       eslint: '>=8'
     dependencies:
-      '@eslint-community/eslint-utils': 4.4.0(eslint@8.57.0)
+      '@eslint-community/eslint-utils': 4.4.0(eslint@9.0.0)
       '@eslint-community/regexpp': 4.9.1
-      eslint: 8.57.0
-      eslint-compat-utils: 0.1.2(eslint@8.57.0)
+      eslint: 9.0.0
+      eslint-compat-utils: 0.1.2(eslint@9.0.0)
     dev: true
 
   /eslint-plugin-es@3.0.1(eslint@8.46.0):
@@ -8213,35 +8399,35 @@ packages:
       regexpp: 3.2.0
     dev: true
 
-  /eslint-plugin-es@4.1.0(eslint@8.57.0):
+  /eslint-plugin-es@4.1.0(eslint@9.0.0):
     resolution: {integrity: sha512-GILhQTnjYE2WorX5Jyi5i4dz5ALWxBIdQECVQavL6s7cI76IZTDWleTHkxz/QT3kvcs2QlGHvKLYsSlPOlPXnQ==}
     engines: {node: '>=8.10.0'}
     peerDependencies:
       eslint: '>=4.19.1'
     dependencies:
-      eslint: 8.57.0
+      eslint: 9.0.0
       eslint-utils: 2.1.0
       regexpp: 3.2.0
     dev: true
 
-  /eslint-plugin-eslint-comments@3.2.0(eslint@8.57.0):
+  /eslint-plugin-eslint-comments@3.2.0(eslint@9.0.0):
     resolution: {integrity: sha512-0jkOl0hfojIHHmEHgmNdqv4fmh7300NdpA9FFpF7zaoLvB/QeXOGNLIo86oAveJFrfB1p05kC8hpEMHM8DwWVQ==}
     engines: {node: '>=6.5.0'}
     peerDependencies:
       eslint: '>=4.19.1'
     dependencies:
       escape-string-regexp: 1.0.5
-      eslint: 8.57.0
+      eslint: 9.0.0
       ignore: 5.2.4
     dev: true
 
-  /eslint-plugin-file-progress@1.3.0(eslint@8.57.0):
+  /eslint-plugin-file-progress@1.3.0(eslint@9.0.0):
     resolution: {integrity: sha512-LncpnGHU26KPvCrvDC2Sl9PfjdrsG8qltgiK6BR7KybWtfqrdlsu1ax3+hyPMn5OkKBTF3Wki3oqK1MSMeOtQw==}
     peerDependencies:
       eslint: ^7.0.0 || ^8.0.0
     dependencies:
       chalk: 4.1.2
-      eslint: 8.57.0
+      eslint: 9.0.0
       ora: 5.4.1
     dev: true
 
@@ -8287,7 +8473,7 @@ packages:
       - supports-color
     dev: true
 
-  /eslint-plugin-import@2.29.0(@typescript-eslint/parser@5.62.0)(eslint@8.57.0):
+  /eslint-plugin-import@2.29.0(@typescript-eslint/parser@5.62.0)(eslint@9.0.0):
     resolution: {integrity: sha512-QPOO5NO6Odv5lpoTkddtutccQjysJuFxoPS7fAHO+9m9udNHvTCPSAMW9zGAYj8lAIdr40I8yPCdUYrncXtrwg==}
     engines: {node: '>=4'}
     peerDependencies:
@@ -8297,16 +8483,16 @@ packages:
       '@typescript-eslint/parser':
         optional: true
     dependencies:
-      '@typescript-eslint/parser': 5.62.0(eslint@8.57.0)(typescript@5.4.5)
+      '@typescript-eslint/parser': 5.62.0(eslint@9.0.0)(typescript@5.4.5)
       array-includes: 3.1.7
       array.prototype.findlastindex: 1.2.3
       array.prototype.flat: 1.3.2
       array.prototype.flatmap: 1.3.2
       debug: 3.2.7
       doctrine: 2.1.0
-      eslint: 8.57.0
+      eslint: 9.0.0
       eslint-import-resolver-node: 0.3.9
-      eslint-module-utils: 2.8.0(@typescript-eslint/parser@5.62.0)(eslint-import-resolver-node@0.3.9)(eslint@8.57.0)
+      eslint-module-utils: 2.8.0(@typescript-eslint/parser@5.62.0)(eslint-import-resolver-node@0.3.9)(eslint@9.0.0)
       hasown: 2.0.0
       is-core-module: 2.13.1
       is-glob: 4.0.3
@@ -8322,7 +8508,7 @@ packages:
       - supports-color
     dev: true
 
-  /eslint-plugin-import@2.29.1(@typescript-eslint/parser@6.21.0)(eslint@8.57.0):
+  /eslint-plugin-import@2.29.1(@typescript-eslint/parser@6.21.0)(eslint@9.0.0):
     resolution: {integrity: sha512-BbPC0cuExzhiMo4Ff1BTVwHpjjv28C5R+btTOGaCRC7UEz801up0JadwkeSk5Ued6TG34uaczuVuH6qyy5YUxw==}
     engines: {node: '>=4'}
     peerDependencies:
@@ -8332,16 +8518,16 @@ packages:
       '@typescript-eslint/parser':
         optional: true
     dependencies:
-      '@typescript-eslint/parser': 6.21.0(eslint@8.57.0)(typescript@5.4.5)
+      '@typescript-eslint/parser': 6.21.0(eslint@9.0.0)(typescript@5.4.5)
       array-includes: 3.1.7
       array.prototype.findlastindex: 1.2.3
       array.prototype.flat: 1.3.2
       array.prototype.flatmap: 1.3.2
       debug: 3.2.7
       doctrine: 2.1.0
-      eslint: 8.57.0
+      eslint: 9.0.0
       eslint-import-resolver-node: 0.3.9
-      eslint-module-utils: 2.8.0(@typescript-eslint/parser@6.21.0)(eslint-import-resolver-node@0.3.9)(eslint@8.57.0)
+      eslint-module-utils: 2.8.0(@typescript-eslint/parser@6.21.0)(eslint-import-resolver-node@0.3.9)(eslint@9.0.0)
       hasown: 2.0.0
       is-core-module: 2.13.1
       is-glob: 4.0.3
@@ -8357,7 +8543,7 @@ packages:
       - supports-color
     dev: true
 
-  /eslint-plugin-jsdoc@48.0.6(eslint@8.57.0):
+  /eslint-plugin-jsdoc@48.0.6(eslint@9.0.0):
     resolution: {integrity: sha512-LgwXOX6TWxxFYcbdVe+BJ94Kl/pgjSPYHLzqEdAMXTA1BH9WDx7iJ+9/iDajPF64LtzWX8C1mCfpbMZjJGhAOw==}
     engines: {node: '>=18'}
     peerDependencies:
@@ -8368,7 +8554,7 @@ packages:
       comment-parser: 1.4.1
       debug: 4.3.4(supports-color@8.1.1)
       escape-string-regexp: 4.0.0
-      eslint: 8.57.0
+      eslint: 9.0.0
       esquery: 1.5.0
       is-builtin-module: 3.2.1
       semver: 7.6.0
@@ -8377,28 +8563,28 @@ packages:
       - supports-color
     dev: true
 
-  /eslint-plugin-jsonc@2.10.0(eslint@8.57.0):
+  /eslint-plugin-jsonc@2.10.0(eslint@9.0.0):
     resolution: {integrity: sha512-9d//o6Jyh4s1RxC9fNSt1+MMaFN2ruFdXPG9XZcb/mR2KkfjADYiNL/hbU6W0Cyxfg3tS/XSFuhl5LgtMD8hmw==}
     engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
     peerDependencies:
       eslint: '>=6.0.0'
     dependencies:
-      '@eslint-community/eslint-utils': 4.4.0(eslint@8.57.0)
-      eslint: 8.57.0
-      eslint-compat-utils: 0.1.2(eslint@8.57.0)
+      '@eslint-community/eslint-utils': 4.4.0(eslint@9.0.0)
+      eslint: 9.0.0
+      eslint-compat-utils: 0.1.2(eslint@9.0.0)
       jsonc-eslint-parser: 2.3.0
       natural-compare: 1.4.0
     dev: true
 
-  /eslint-plugin-jsonc@2.13.0(eslint@8.57.0):
+  /eslint-plugin-jsonc@2.13.0(eslint@9.0.0):
     resolution: {integrity: sha512-2wWdJfpO/UbZzPDABuUVvlUQjfMJa2p2iQfYt/oWxOMpXCcjuiMUSaA02gtY/Dbu82vpaSqc+O7Xq6ECHwtIxA==}
     engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
     peerDependencies:
       eslint: '>=6.0.0'
     dependencies:
-      '@eslint-community/eslint-utils': 4.4.0(eslint@8.57.0)
-      eslint: 8.57.0
-      eslint-compat-utils: 0.4.1(eslint@8.57.0)
+      '@eslint-community/eslint-utils': 4.4.0(eslint@9.0.0)
+      eslint: 9.0.0
+      eslint-compat-utils: 0.4.1(eslint@9.0.0)
       espree: 9.6.1
       graphemer: 1.4.0
       jsonc-eslint-parser: 2.4.0
@@ -8406,28 +8592,28 @@ packages:
       synckit: 0.6.2
     dev: true
 
-  /eslint-plugin-markdown@3.0.1(eslint@8.57.0):
+  /eslint-plugin-markdown@3.0.1(eslint@9.0.0):
     resolution: {integrity: sha512-8rqoc148DWdGdmYF6WSQFT3uQ6PO7zXYgeBpHAOAakX/zpq+NvFYbDA/H7PYzHajwtmaOzAwfxyl++x0g1/N9A==}
     engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
     peerDependencies:
       eslint: ^6.0.0 || ^7.0.0 || ^8.0.0
     dependencies:
-      eslint: 8.57.0
+      eslint: 9.0.0
       mdast-util-from-markdown: 0.8.5
     transitivePeerDependencies:
       - supports-color
     dev: true
 
-  /eslint-plugin-n@15.7.0(eslint@8.57.0):
+  /eslint-plugin-n@15.7.0(eslint@9.0.0):
     resolution: {integrity: sha512-jDex9s7D/Qial8AGVIHq4W7NswpUD5DPDL2RH8Lzd9EloWUuvUkHfv4FRLMipH5q2UtyurorBkPeNi1wVWNh3Q==}
     engines: {node: '>=12.22.0'}
     peerDependencies:
       eslint: '>=7.0.0'
     dependencies:
       builtins: 5.0.1
-      eslint: 8.57.0
-      eslint-plugin-es: 4.1.0(eslint@8.57.0)
-      eslint-utils: 3.0.0(eslint@8.57.0)
+      eslint: 9.0.0
+      eslint-plugin-es: 4.1.0(eslint@9.0.0)
+      eslint-utils: 3.0.0(eslint@9.0.0)
       ignore: 5.2.4
       is-core-module: 2.13.1
       minimatch: 3.1.2
@@ -8435,16 +8621,16 @@ packages:
       semver: 7.6.0
     dev: true
 
-  /eslint-plugin-n@16.6.2(eslint@8.57.0):
+  /eslint-plugin-n@16.6.2(eslint@9.0.0):
     resolution: {integrity: sha512-6TyDmZ1HXoFQXnhCTUjVFULReoBPOAjpuiKELMkeP40yffI/1ZRO+d9ug/VC6fqISo2WkuIBk3cvuRPALaWlOQ==}
     engines: {node: '>=16.0.0'}
     peerDependencies:
       eslint: '>=7.0.0'
     dependencies:
-      '@eslint-community/eslint-utils': 4.4.0(eslint@8.57.0)
+      '@eslint-community/eslint-utils': 4.4.0(eslint@9.0.0)
       builtins: 5.0.1
-      eslint: 8.57.0
-      eslint-plugin-es-x: 7.5.0(eslint@8.57.0)
+      eslint: 9.0.0
+      eslint-plugin-es-x: 7.5.0(eslint@9.0.0)
       get-tsconfig: 4.7.2
       globals: 13.24.0
       ignore: 5.2.4
@@ -8470,7 +8656,7 @@ packages:
       semver: 6.3.1
     dev: true
 
-  /eslint-plugin-prettier@4.2.1(eslint-config-prettier@8.9.0)(eslint@8.57.0)(prettier@3.2.5):
+  /eslint-plugin-prettier@4.2.1(eslint-config-prettier@8.9.0)(eslint@9.0.0)(prettier@3.2.5):
     resolution: {integrity: sha512-f/0rXLXUt0oFYs8ra4w49wYZBG5GKZpAYsJSm6rnYL5uVDjd+zowwMwVZHnAjf4edNrKpCDYfXDgmRE/Ak7QyQ==}
     engines: {node: '>=12.0.0'}
     peerDependencies:
@@ -8481,13 +8667,13 @@ packages:
       eslint-config-prettier:
         optional: true
     dependencies:
-      eslint: 8.57.0
-      eslint-config-prettier: 8.9.0(eslint@8.57.0)
+      eslint: 9.0.0
+      eslint-config-prettier: 8.9.0(eslint@9.0.0)
       prettier: 3.2.5
       prettier-linter-helpers: 1.0.0
     dev: true
 
-  /eslint-plugin-prettier@5.1.3(eslint-config-prettier@9.1.0)(eslint@8.57.0)(prettier@3.2.5):
+  /eslint-plugin-prettier@5.1.3(eslint-config-prettier@9.1.0)(eslint@9.0.0)(prettier@3.2.5):
     resolution: {integrity: sha512-C9GCVAs4Eq7ZC/XFQHITLiHJxQngdtraXaM+LoUFoFp/lHNl2Zn8f3WQbe9HvTBBQ9YnKFB0/2Ajdqwo5D1EAw==}
     engines: {node: ^14.18.0 || >=16.0.0}
     peerDependencies:
@@ -8501,20 +8687,20 @@ packages:
       eslint-config-prettier:
         optional: true
     dependencies:
-      eslint: 8.57.0
-      eslint-config-prettier: 9.1.0(eslint@8.57.0)
+      eslint: 9.0.0
+      eslint-config-prettier: 9.1.0(eslint@9.0.0)
       prettier: 3.2.5
       prettier-linter-helpers: 1.0.0
       synckit: 0.8.8
     dev: true
 
-  /eslint-plugin-promise@5.2.0(eslint@8.57.0):
+  /eslint-plugin-promise@5.2.0(eslint@9.0.0):
     resolution: {integrity: sha512-SftLb1pUG01QYq2A/hGAWfDRXqYD82zE7j7TopDOyNdU+7SvvoXREls/+PRTY17vUXzXnZA/zfnyKgRH6x4JJw==}
     engines: {node: ^10.12.0 || >=12.0.0}
     peerDependencies:
       eslint: ^7.0.0
     dependencies:
-      eslint: 8.57.0
+      eslint: 9.0.0
     dev: true
 
   /eslint-plugin-promise@6.1.1(eslint@8.46.0):
@@ -8526,13 +8712,13 @@ packages:
       eslint: 8.46.0
     dev: true
 
-  /eslint-plugin-promise@6.1.1(eslint@8.57.0):
+  /eslint-plugin-promise@6.1.1(eslint@9.0.0):
     resolution: {integrity: sha512-tjqWDwVZQo7UIPMeDReOpUgHCmCiH+ePnVT+5zVapL0uuHnegBUs2smM13CzOs2Xb5+MHMRFTs9v24yjba4Oig==}
     engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
     peerDependencies:
       eslint: ^7.0.0 || ^8.0.0
     dependencies:
-      eslint: 8.57.0
+      eslint: 9.0.0
     dev: true
 
   /eslint-plugin-standard@5.0.0(eslint@8.46.0):
@@ -8551,7 +8737,7 @@ packages:
       '@microsoft/tsdoc-config': 0.16.2
     dev: true
 
-  /eslint-plugin-unicorn@40.1.0(eslint@8.57.0):
+  /eslint-plugin-unicorn@40.1.0(eslint@9.0.0):
     resolution: {integrity: sha512-y5doK2DF9Sr5AqKEHbHxjFllJ167nKDRU01HDcWyv4Tnmaoe9iNxMrBnaybZvWZUaE3OC5Unu0lNIevYamloig==}
     engines: {node: '>=12'}
     peerDependencies:
@@ -8560,8 +8746,8 @@ packages:
       '@babel/helper-validator-identifier': 7.22.20
       ci-info: 3.9.0
       clean-regexp: 1.0.0
-      eslint: 8.57.0
-      eslint-utils: 3.0.0(eslint@8.57.0)
+      eslint: 9.0.0
+      eslint-utils: 3.0.0(eslint@9.0.0)
       esquery: 1.5.0
       indent-string: 4.0.0
       is-builtin-module: 3.2.1
@@ -8574,17 +8760,17 @@ packages:
       strip-indent: 3.0.0
     dev: true
 
-  /eslint-plugin-unicorn@45.0.2(eslint@8.57.0):
+  /eslint-plugin-unicorn@45.0.2(eslint@9.0.0):
     resolution: {integrity: sha512-Y0WUDXRyGDMcKLiwgL3zSMpHrXI00xmdyixEGIg90gHnj0PcHY4moNv3Ppje/kDivdAy5vUeUr7z211ImPv2gw==}
     engines: {node: '>=14.18'}
     peerDependencies:
       eslint: '>=8.28.0'
     dependencies:
       '@babel/helper-validator-identifier': 7.22.5
-      '@eslint-community/eslint-utils': 4.4.0(eslint@8.57.0)
+      '@eslint-community/eslint-utils': 4.4.0(eslint@9.0.0)
       ci-info: 3.9.0
       clean-regexp: 1.0.0
-      eslint: 8.57.0
+      eslint: 9.0.0
       esquery: 1.5.0
       indent-string: 4.0.0
       is-builtin-module: 3.2.1
@@ -8603,92 +8789,92 @@ packages:
     resolution: {integrity: sha512-WE+YlK9X9s4vf5EaYRU0Scw7WItDZStm+PapFSYlg2ABNtaQ4zIG7wEqpoUB3SlfM+SgkhgmzR0TeJOO5k3/Nw==}
     dev: true
 
-  /eslint-plugin-vue-scoped-css@2.5.0(eslint@8.57.0)(vue-eslint-parser@9.3.1):
+  /eslint-plugin-vue-scoped-css@2.5.0(eslint@9.0.0)(vue-eslint-parser@9.3.1):
     resolution: {integrity: sha512-vR+raYNE1aQ69lS1lZGiKoz8rXFI3MWf2fxrfns/XCQ0XT5sIguhDtQS+9JmUQJClenLDEe2CQx7P+eeSdF4cA==}
     engines: {node: ^12.22 || ^14.17 || >=16}
     peerDependencies:
       eslint: '>=5.0.0'
       vue-eslint-parser: '>=7.1.0'
     dependencies:
-      eslint: 8.57.0
-      eslint-utils: 3.0.0(eslint@8.57.0)
+      eslint: 9.0.0
+      eslint-utils: 3.0.0(eslint@9.0.0)
       lodash: 4.17.21
       postcss: 8.4.31
       postcss-safe-parser: 6.0.0(postcss@8.4.31)
       postcss-scss: 4.0.6(postcss@8.4.31)
       postcss-selector-parser: 6.0.13
       postcss-styl: 0.12.3
-      vue-eslint-parser: 9.3.1(eslint@8.57.0)
+      vue-eslint-parser: 9.3.1(eslint@9.0.0)
     transitivePeerDependencies:
       - supports-color
     dev: true
 
-  /eslint-plugin-vue-scoped-css@2.7.2(eslint@8.57.0)(vue-eslint-parser@9.4.2):
+  /eslint-plugin-vue-scoped-css@2.7.2(eslint@9.0.0)(vue-eslint-parser@9.4.2):
     resolution: {integrity: sha512-myJ99CJuwmAx5kq1WjgIeaUkxeU6PIEUh7age79Alm30bhN4fVTapOQLSMlvVTgxr36Y3igsZ3BCJM32LbHHig==}
     engines: {node: ^12.22 || ^14.17 || >=16}
     peerDependencies:
       eslint: '>=5.0.0'
       vue-eslint-parser: '>=7.1.0'
     dependencies:
-      '@eslint-community/eslint-utils': 4.4.0(eslint@8.57.0)
-      eslint: 8.57.0
-      eslint-compat-utils: 0.4.1(eslint@8.57.0)
+      '@eslint-community/eslint-utils': 4.4.0(eslint@9.0.0)
+      eslint: 9.0.0
+      eslint-compat-utils: 0.4.1(eslint@9.0.0)
       lodash: 4.17.21
       postcss: 8.4.31
       postcss-safe-parser: 6.0.0(postcss@8.4.31)
       postcss-scss: 4.0.6(postcss@8.4.31)
       postcss-selector-parser: 6.0.13
       postcss-styl: 0.12.3
-      vue-eslint-parser: 9.4.2(eslint@8.57.0)
+      vue-eslint-parser: 9.4.2(eslint@9.0.0)
     transitivePeerDependencies:
       - supports-color
     dev: true
 
-  /eslint-plugin-vue@9.16.1(eslint@8.57.0):
+  /eslint-plugin-vue@9.16.1(eslint@9.0.0):
     resolution: {integrity: sha512-2FtnTqazA6aYONfDuOZTk0QzwhAwi7Z4+uJ7+GHeGxcKapjqWlDsRWDenvyG/utyOfAS5bVRmAG3cEWiYEz2bA==}
     engines: {node: ^14.17.0 || >=16.0.0}
     peerDependencies:
       eslint: ^6.2.0 || ^7.0.0 || ^8.0.0
     dependencies:
-      '@eslint-community/eslint-utils': 4.4.0(eslint@8.57.0)
-      eslint: 8.57.0
+      '@eslint-community/eslint-utils': 4.4.0(eslint@9.0.0)
+      eslint: 9.0.0
       natural-compare: 1.4.0
       nth-check: 2.1.1
       postcss-selector-parser: 6.0.13
       semver: 7.6.0
-      vue-eslint-parser: 9.3.2(eslint@8.57.0)
+      vue-eslint-parser: 9.3.2(eslint@9.0.0)
       xml-name-validator: 4.0.0
     transitivePeerDependencies:
       - supports-color
     dev: true
 
-  /eslint-plugin-vue@9.21.1(eslint@8.57.0):
+  /eslint-plugin-vue@9.21.1(eslint@9.0.0):
     resolution: {integrity: sha512-XVtI7z39yOVBFJyi8Ljbn7kY9yHzznKXL02qQYn+ta63Iy4A9JFBw6o4OSB9hyD2++tVT+su9kQqetUyCCwhjw==}
     engines: {node: ^14.17.0 || >=16.0.0}
     peerDependencies:
       eslint: ^6.2.0 || ^7.0.0 || ^8.0.0
     dependencies:
-      '@eslint-community/eslint-utils': 4.4.0(eslint@8.57.0)
-      eslint: 8.57.0
+      '@eslint-community/eslint-utils': 4.4.0(eslint@9.0.0)
+      eslint: 9.0.0
       natural-compare: 1.4.0
       nth-check: 2.1.1
       postcss-selector-parser: 6.0.13
       semver: 7.6.0
-      vue-eslint-parser: 9.4.2(eslint@8.57.0)
+      vue-eslint-parser: 9.4.2(eslint@9.0.0)
       xml-name-validator: 4.0.0
     transitivePeerDependencies:
       - supports-color
     dev: true
 
-  /eslint-plugin-yml@1.10.0(eslint@8.57.0):
+  /eslint-plugin-yml@1.10.0(eslint@9.0.0):
     resolution: {integrity: sha512-53SUwuNDna97lVk38hL/5++WXDuugPM9SUQ1T645R0EHMRCdBIIxGye/oOX2qO3FQ7aImxaUZJU/ju+NMUBrLQ==}
     engines: {node: ^14.17.0 || >=16.0.0}
     peerDependencies:
       eslint: '>=6.0.0'
     dependencies:
       debug: 4.3.4(supports-color@8.1.1)
-      eslint: 8.57.0
-      eslint-compat-utils: 0.1.2(eslint@8.57.0)
+      eslint: 9.0.0
+      eslint-compat-utils: 0.1.2(eslint@9.0.0)
       lodash: 4.17.21
       natural-compare: 1.4.0
       yaml-eslint-parser: 1.2.2
@@ -8696,15 +8882,15 @@ packages:
       - supports-color
     dev: true
 
-  /eslint-plugin-yml@1.12.2(eslint@8.57.0):
+  /eslint-plugin-yml@1.12.2(eslint@9.0.0):
     resolution: {integrity: sha512-hvS9p08FhPT7i/ynwl7/Wt7ke7Rf4P2D6fT8lZlL43peZDTsHtH2A0SIFQ7Kt7+mJ6if6P+FX3iJhMkdnxQwpg==}
     engines: {node: ^14.17.0 || >=16.0.0}
     peerDependencies:
       eslint: '>=6.0.0'
     dependencies:
       debug: 4.3.4(supports-color@8.1.1)
-      eslint: 8.57.0
-      eslint-compat-utils: 0.4.1(eslint@8.57.0)
+      eslint: 9.0.0
+      eslint-compat-utils: 0.4.1(eslint@9.0.0)
       lodash: 4.17.21
       natural-compare: 1.4.0
       yaml-eslint-parser: 1.2.2
@@ -8747,13 +8933,13 @@ packages:
       eslint-visitor-keys: 1.3.0
     dev: true
 
-  /eslint-utils@3.0.0(eslint@8.57.0):
+  /eslint-utils@3.0.0(eslint@9.0.0):
     resolution: {integrity: sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==}
     engines: {node: ^10.0.0 || ^12.0.0 || >= 14.0.0}
     peerDependencies:
       eslint: '>=5'
     dependencies:
-      eslint: 8.57.0
+      eslint: 9.0.0
       eslint-visitor-keys: 2.1.0
     dev: true
 
@@ -8828,53 +9014,6 @@ packages:
       - supports-color
     dev: true
 
-  /eslint@8.57.0:
-    resolution: {integrity: sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==}
-    engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
-    hasBin: true
-    dependencies:
-      '@eslint-community/eslint-utils': 4.4.0(eslint@8.57.0)
-      '@eslint-community/regexpp': 4.9.1
-      '@eslint/eslintrc': 2.1.4
-      '@eslint/js': 8.57.0
-      '@humanwhocodes/config-array': 0.11.14
-      '@humanwhocodes/module-importer': 1.0.1
-      '@nodelib/fs.walk': 1.2.8
-      '@ungap/structured-clone': 1.2.0
-      ajv: 6.12.6
-      chalk: 4.1.2
-      cross-spawn: 7.0.3
-      debug: 4.3.4(supports-color@8.1.1)
-      doctrine: 3.0.0
-      escape-string-regexp: 4.0.0
-      eslint-scope: 7.2.2
-      eslint-visitor-keys: 3.4.3
-      espree: 9.6.1
-      esquery: 1.5.0
-      esutils: 2.0.3
-      fast-deep-equal: 3.1.3
-      file-entry-cache: 6.0.1
-      find-up: 5.0.0
-      glob-parent: 6.0.2
-      globals: 13.24.0
-      graphemer: 1.4.0
-      ignore: 5.2.4
-      imurmurhash: 0.1.4
-      is-glob: 4.0.3
-      is-path-inside: 3.0.3
-      js-yaml: 4.1.0
-      json-stable-stringify-without-jsonify: 1.0.1
-      levn: 0.4.1
-      lodash.merge: 4.6.2
-      minimatch: 3.1.2
-      natural-compare: 1.4.0
-      optionator: 0.9.3
-      strip-ansi: 6.0.1
-      text-table: 0.2.0
-    transitivePeerDependencies:
-      - supports-color
-    dev: true
-
   /eslint@9.0.0:
     resolution: {integrity: sha512-IMryZ5SudxzQvuod6rUdIUz29qFItWx281VhtFVc2Psy/ZhlCeD/5DT6lBIJ4H3G+iamGJoTln1v+QSuPw0p7Q==}
     engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
@@ -9584,7 +9723,7 @@ packages:
       dezalgo: 1.0.4
       hexoid: 1.0.0
       once: 1.4.0
-      qs: 6.12.0
+      qs: 6.12.1
     dev: false
 
   /fragment-cache@0.2.1:
@@ -9648,7 +9787,7 @@ packages:
     requiresBuild: true
     dependencies:
       bindings: 1.5.0
-      nan: 2.18.0
+      nan: 2.19.0
     dev: false
     optional: true
 
@@ -9727,9 +9866,9 @@ packages:
     dependencies:
       es-errors: 1.3.0
       function-bind: 1.1.2
-      has-proto: 1.0.1
+      has-proto: 1.0.3
       has-symbols: 1.0.3
-      hasown: 2.0.0
+      hasown: 2.0.2
 
   /get-package-type@0.1.0:
     resolution: {integrity: sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==}
@@ -10183,6 +10322,10 @@ packages:
     resolution: {integrity: sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==}
     engines: {node: '>= 0.4'}
 
+  /has-proto@1.0.3:
+    resolution: {integrity: sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==}
+    engines: {node: '>= 0.4'}
+
   /has-symbols@1.0.3:
     resolution: {integrity: sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==}
     engines: {node: '>= 0.4'}
@@ -10193,6 +10336,12 @@ packages:
     dependencies:
       has-symbols: 1.0.3
 
+  /has-tostringtag@1.0.2:
+    resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==}
+    engines: {node: '>= 0.4'}
+    dependencies:
+      has-symbols: 1.0.3
+
   /has-unicode@2.0.1:
     resolution: {integrity: sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==}
     dev: true
@@ -10240,6 +10389,12 @@ packages:
     dependencies:
       function-bind: 1.1.2
 
+  /hasown@2.0.2:
+    resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
+    engines: {node: '>= 0.4'}
+    dependencies:
+      function-bind: 1.1.2
+
   /he@1.2.0:
     resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==}
     hasBin: true
@@ -10437,8 +10592,8 @@ packages:
     engines: {node: '>= 4'}
     dev: true
 
-  /immutable@4.3.2:
-    resolution: {integrity: sha512-oGXzbEDem9OOpDWZu88jGiYCvIsLHMvGw+8OXlpsvTFvIQplQbjg1B1cvKg8f7Hoch6+NGjpPsH1Fr+Mc2D1aA==}
+  /immutable@4.3.5:
+    resolution: {integrity: sha512-8eabxkth9gZatlwl5TBuJnCsoTADlL6ftEr7A4qgdaTsPyreilDSnUk57SO+jfKcNtxPa22U5KK6DSeAYhpBJw==}
     dev: true
 
   /import-fresh@3.3.0:
@@ -10595,7 +10750,7 @@ packages:
     resolution: {integrity: sha512-YBUanLI8Yoihw923YeFUS5fs0fF2f5TSFTNiYAAzhhDscDa3lEqYuz1pDOEP5KvX94I9ey3vsqjJcLVFVU+3QA==}
     engines: {node: '>= 0.10'}
     dependencies:
-      hasown: 2.0.0
+      hasown: 2.0.2
     dev: false
 
   /is-alphabetical@1.0.4:
@@ -10614,7 +10769,7 @@ packages:
     engines: {node: '>= 0.4'}
     dependencies:
       call-bind: 1.0.7
-      has-tostringtag: 1.0.0
+      has-tostringtag: 1.0.2
     dev: false
 
   /is-array-buffer@3.0.2:
@@ -10657,7 +10812,7 @@ packages:
     engines: {node: '>= 0.4'}
     dependencies:
       call-bind: 1.0.2
-      has-tostringtag: 1.0.0
+      has-tostringtag: 1.0.2
     dev: true
 
   /is-buffer@1.1.6:
@@ -10689,14 +10844,14 @@ packages:
     resolution: {integrity: sha512-bc4NlCDiCr28U4aEsQ3Qs2491gVq4V8G7MQyws968ImqjKuYtTJXrl7Vq7jsN7Ly/C3xj5KWFrY7sHNeDkAzXw==}
     engines: {node: '>= 0.4'}
     dependencies:
-      hasown: 2.0.0
+      hasown: 2.0.2
     dev: false
 
   /is-date-object@1.0.5:
     resolution: {integrity: sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==}
     engines: {node: '>= 0.4'}
     dependencies:
-      has-tostringtag: 1.0.0
+      has-tostringtag: 1.0.2
     dev: true
 
   /is-decimal@1.0.4:
@@ -10829,7 +10984,7 @@ packages:
     resolution: {integrity: sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==}
     engines: {node: '>= 0.4'}
     dependencies:
-      has-tostringtag: 1.0.0
+      has-tostringtag: 1.0.2
     dev: true
 
   /is-number@3.0.0:
@@ -10947,6 +11102,14 @@ packages:
     engines: {node: '>= 0.4'}
     dependencies:
       which-typed-array: 1.1.11
+    dev: true
+
+  /is-typed-array@1.1.13:
+    resolution: {integrity: sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw==}
+    engines: {node: '>= 0.4'}
+    dependencies:
+      which-typed-array: 1.1.15
+    dev: false
 
   /is-typedarray@1.0.0:
     resolution: {integrity: sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==}
@@ -11102,7 +11265,7 @@ packages:
     engines: {node: '>=10'}
     hasBin: true
     dependencies:
-      async: 3.2.4
+      async: 3.2.5
       chalk: 4.1.2
       filelist: 1.0.4
       minimatch: 3.1.2
@@ -12002,7 +12165,7 @@ packages:
       http-errors: 1.8.1
       koa-compose: 4.1.0
       methods: 1.1.2
-      path-to-regexp: 6.2.1
+      path-to-regexp: 6.2.2
     transitivePeerDependencies:
       - supports-color
     dev: false
@@ -12036,7 +12199,7 @@ packages:
       - supports-color
     dev: false
 
-  /koa-views@7.0.2(@types/koa@2.15.0)(ejs@3.1.9)(pug@3.0.2):
+  /koa-views@7.0.2(@types/koa@2.15.0)(ejs@3.1.10)(pug@3.0.2):
     resolution: {integrity: sha512-dvx3mdVeSVuIPEaKAoGbxLcenudvhl821xxyuRbcoA+bOJ2dvN8wlGjkLu0ZFMlkCscXZV6lzxy28rafeazI/w==}
     deprecated: This package is deprecated, please use the new fork @ladjs/koa-views. Maintenance is supported by Forward Email at https://forwardemail.net ; follow/watch https://github.com/ladjs/koa-views for updates and release changelog
     peerDependencies:
@@ -12046,7 +12209,7 @@ packages:
         optional: true
     dependencies:
       '@types/koa': 2.15.0
-      consolidate: 0.16.0(ejs@3.1.9)(pug@3.0.2)
+      consolidate: 0.16.0(ejs@3.1.10)(pug@3.0.2)
       debug: 4.3.4(supports-color@8.1.1)
       get-paths: 0.0.7
       koa-send: 5.0.1
@@ -12275,8 +12438,8 @@ packages:
     engines: {node: '>=6.11.5'}
     dev: true
 
-  /loadjs@4.2.0:
-    resolution: {integrity: sha512-AgQGZisAlTPbTEzrHPb6q+NYBMD+DP9uvGSIjSUM5uG+0jG15cb8axWpxuOIqrmQjn6scaaH8JwloiP27b2KXA==}
+  /loadjs@4.3.0:
+    resolution: {integrity: sha512-vNX4ZZLJBeDEOBvdr2v/F+0aN5oMuPu7JTqrMwp+DtgK+AryOlpy6Xtm2/HpNr+azEa828oQjOtWsB6iDtSfSQ==}
     dev: true
 
   /local-pkg@0.5.0:
@@ -12727,6 +12890,13 @@ packages:
     dependencies:
       brace-expansion: 2.0.1
 
+  /minimatch@9.0.4:
+    resolution: {integrity: sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==}
+    engines: {node: '>=16 || 14 >=14.17'}
+    dependencies:
+      brace-expansion: 2.0.1
+    dev: true
+
   /minimist-options@4.1.0:
     resolution: {integrity: sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A==}
     engines: {node: '>= 6'}
@@ -12903,8 +13073,8 @@ packages:
       thenify-all: 1.6.0
     dev: false
 
-  /nan@2.18.0:
-    resolution: {integrity: sha512-W7tfG7vMOGtD30sHoZSSc/JVYiyDPEyQVso/Zz+/uQd0B0L46gtC+pHha5FFMRpil6fm/AoEcRWyOVi4+E/f8w==}
+  /nan@2.19.0:
+    resolution: {integrity: sha512-nO1xXxfh/RWNxfd/XPfbIfFk5vgLsAxUR9y5O0cHMJu/AW9U95JLXqthYHjEp+8gQ5p96K9jUp8nbVOxCdRbtw==}
     requiresBuild: true
     dev: false
     optional: true
@@ -13055,6 +13225,10 @@ packages:
   /node-releases@2.0.13:
     resolution: {integrity: sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==}
 
+  /node-releases@2.0.14:
+    resolution: {integrity: sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==}
+    dev: true
+
   /nodemailer@6.9.13:
     resolution: {integrity: sha512-7o38Yogx6krdoBf3jCAqnIN4oSQFx+fMa0I7dK1D+me9kBxx12D+/33wSb+fhOCtIxvYJ+4x4IMEhmhCKfAiOA==}
     engines: {node: '>=6.0.0'}
@@ -13682,6 +13856,10 @@ packages:
     resolution: {integrity: sha512-JLyh7xT1kizaEvcaXOQwOc2/Yhw6KZOvPf1S8401UyLk86CU79LN3vl7ztXGm/pZ+YjoyAJ4rxmHwbkBXJX+yw==}
     dev: false
 
+  /path-to-regexp@6.2.2:
+    resolution: {integrity: sha512-GQX3SSMokngb36+whdpRXE+3f9V8UzyAorlYvOGx87ufGHehNTn5lCxrKtLyZ4Yl/wEKnNnr98ZzOwwDZV5ogw==}
+    dev: false
+
   /path-type@1.1.0:
     resolution: {integrity: sha512-S4eENJz1pkiQn9Znv33Q+deTOKmbl+jj1Fl+qiP/vYezj+S8x+J3Uo0ISrx/QoEvIlOaDWJhPaRd1flJ9HXZqg==}
     engines: {node: '>=0.10.0'}
@@ -13907,8 +14085,8 @@ packages:
     engines: {node: '>=14.19.0'}
     dev: false
 
-  /pnpm@8.15.6:
-    resolution: {integrity: sha512-d7iem+d6Kwatj0A6Gcrl4il29hAj+YrTI9XDAZSVjrwC7gpq5dE+5FT2E05OjK8poF8LGg4dKxe8prah8RWfhg==}
+  /pnpm@8.15.7:
+    resolution: {integrity: sha512-yFzSG22hAzIVaxyiqnnAph7nrS6wRTuIqymSienoypPmCRIyslwHy/YfbfdxKNnISeXJrG5EhU29IRxJ86Z63A==}
     engines: {node: '>=16.14'}
     hasBin: true
     dev: true
@@ -13918,6 +14096,11 @@ packages:
     engines: {node: '>=0.10.0'}
     dev: false
 
+  /possible-typed-array-names@1.0.0:
+    resolution: {integrity: sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==}
+    engines: {node: '>= 0.4'}
+    dev: false
+
   /postcss-calc@5.3.1:
     resolution: {integrity: sha512-iBcptYFq+QUh9gzP7ta2btw50o40s4uLI4UDVgd5yRAZtUDWc5APdl5yQDd2h/TyiZNbJrv0HiYhT102CMgN7Q==}
     dependencies:
@@ -14512,8 +14695,8 @@ packages:
       yargs: 15.4.1
     dev: false
 
-  /qs@6.12.0:
-    resolution: {integrity: sha512-trVZiI6RMOkO476zLGaBIzszOdFPnCCXHPG9kn0yuS1uz6xdVxPfZdB3vUig9pxPFDM9BRAgz/YUIVQ1/vuiUg==}
+  /qs@6.12.1:
+    resolution: {integrity: sha512-zWmv4RSuB9r2mYQw3zxQuHWeU+42aKi1wWig/j4ele4ygELZ7PEO6MM7rim9oAQH2A5MWfsAVf/jPvTPgCbvUQ==}
     engines: {node: '>=0.6'}
     dependencies:
       side-channel: 1.0.6
@@ -15009,28 +15192,28 @@ packages:
       seedrandom: 2.4.2
     dev: false
 
-  /rollup@4.14.1:
-    resolution: {integrity: sha512-4LnHSdd3QK2pa1J6dFbfm1HN0D7vSK/ZuZTsdyUAlA6Rr1yTouUTL13HaDOGJVgby461AhrNGBS7sCGXXtT+SA==}
+  /rollup@4.14.2:
+    resolution: {integrity: sha512-WkeoTWvuBoFjFAhsEOHKRoZ3r9GfTyhh7Vff1zwebEFLEFjT1lG3784xEgKiTa7E+e70vsC81roVL2MP4tgEEQ==}
     engines: {node: '>=18.0.0', npm: '>=8.0.0'}
     hasBin: true
     dependencies:
       '@types/estree': 1.0.5
     optionalDependencies:
-      '@rollup/rollup-android-arm-eabi': 4.14.1
-      '@rollup/rollup-android-arm64': 4.14.1
-      '@rollup/rollup-darwin-arm64': 4.14.1
-      '@rollup/rollup-darwin-x64': 4.14.1
-      '@rollup/rollup-linux-arm-gnueabihf': 4.14.1
-      '@rollup/rollup-linux-arm64-gnu': 4.14.1
-      '@rollup/rollup-linux-arm64-musl': 4.14.1
-      '@rollup/rollup-linux-powerpc64le-gnu': 4.14.1
-      '@rollup/rollup-linux-riscv64-gnu': 4.14.1
-      '@rollup/rollup-linux-s390x-gnu': 4.14.1
-      '@rollup/rollup-linux-x64-gnu': 4.14.1
-      '@rollup/rollup-linux-x64-musl': 4.14.1
-      '@rollup/rollup-win32-arm64-msvc': 4.14.1
-      '@rollup/rollup-win32-ia32-msvc': 4.14.1
-      '@rollup/rollup-win32-x64-msvc': 4.14.1
+      '@rollup/rollup-android-arm-eabi': 4.14.2
+      '@rollup/rollup-android-arm64': 4.14.2
+      '@rollup/rollup-darwin-arm64': 4.14.2
+      '@rollup/rollup-darwin-x64': 4.14.2
+      '@rollup/rollup-linux-arm-gnueabihf': 4.14.2
+      '@rollup/rollup-linux-arm64-gnu': 4.14.2
+      '@rollup/rollup-linux-arm64-musl': 4.14.2
+      '@rollup/rollup-linux-powerpc64le-gnu': 4.14.2
+      '@rollup/rollup-linux-riscv64-gnu': 4.14.2
+      '@rollup/rollup-linux-s390x-gnu': 4.14.2
+      '@rollup/rollup-linux-x64-gnu': 4.14.2
+      '@rollup/rollup-linux-x64-musl': 4.14.2
+      '@rollup/rollup-win32-arm64-msvc': 4.14.2
+      '@rollup/rollup-win32-ia32-msvc': 4.14.2
+      '@rollup/rollup-win32-x64-msvc': 4.14.2
       fsevents: 2.3.3
     dev: true
 
@@ -15102,13 +15285,13 @@ packages:
       postcss: 8.4.35
     dev: false
 
-  /sass@1.74.1:
-    resolution: {integrity: sha512-w0Z9p/rWZWelb88ISOLyvqTWGmtmu2QJICqDBGyNnfG4OUnPX9BBjjYIXUpXCMOOg5MQWNpqzt876la1fsTvUA==}
+  /sass@1.75.0:
+    resolution: {integrity: sha512-ShMYi3WkrDWxExyxSZPst4/okE9ts46xZmJDSawJQrnte7M1V9fScVB+uNXOVKRBt0PggHOwoZcn8mYX4trnBw==}
     engines: {node: '>=14.0.0'}
     hasBin: true
     dependencies:
-      chokidar: 3.5.3
-      immutable: 4.3.2
+      chokidar: 3.6.0
+      immutable: 4.3.5
       source-map-js: 1.2.0
     dev: true
 
@@ -16257,7 +16440,7 @@ packages:
       typescript: 5.4.5
     dev: true
 
-  /ts-jest@29.1.1(@babel/core@7.23.2)(jest@29.7.0)(typescript@4.9.4):
+  /ts-jest@29.1.1(@babel/core@7.24.4)(jest@29.7.0)(typescript@4.9.4):
     resolution: {integrity: sha512-D6xjnnbP17cC85nliwGiL+tpoKN0StpgE0TeOjXQTU6MVCfsB4v7aW05CgQ/1OywGb0x/oy9hHFnN+sczTiRaA==}
     engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
     hasBin: true
@@ -16278,7 +16461,7 @@ packages:
       esbuild:
         optional: true
     dependencies:
-      '@babel/core': 7.23.2
+      '@babel/core': 7.24.4
       bs-logger: 0.2.6
       fast-json-stable-stringify: 2.1.0
       jest: 29.7.0(@types/node@18.11.18)
@@ -16806,6 +16989,17 @@ packages:
       escalade: 3.1.1
       picocolors: 1.0.0
 
+  /update-browserslist-db@1.0.13(browserslist@4.23.0):
+    resolution: {integrity: sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==}
+    hasBin: true
+    peerDependencies:
+      browserslist: '>= 4.21.0'
+    dependencies:
+      browserslist: 4.23.0
+      escalade: 3.1.1
+      picocolors: 1.0.0
+    dev: true
+
   /uri-js@4.4.1:
     resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
     dependencies:
@@ -16849,8 +17043,8 @@ packages:
       inherits: 2.0.4
       is-arguments: 1.1.1
       is-generator-function: 1.0.10
-      is-typed-array: 1.1.12
-      which-typed-array: 1.1.11
+      is-typed-array: 1.1.13
+      which-typed-array: 1.1.15
     dev: false
 
   /uuid@8.0.0:
@@ -16982,12 +17176,12 @@ packages:
       chalk: 4.1.2
       debug: 4.3.4(supports-color@8.1.1)
       fs-extra: 10.1.0
-      vite: 5.2.8(@types/node@20.12.7)(sass@1.74.1)
+      vite: 5.2.8(@types/node@20.12.7)(sass@1.75.0)
     transitivePeerDependencies:
       - supports-color
     dev: true
 
-  /vite@5.2.8(@types/node@20.12.7)(sass@1.74.1):
+  /vite@5.2.8(@types/node@20.12.7)(sass@1.75.0):
     resolution: {integrity: sha512-OyZR+c1CE8yeHw5V5t59aXsUPPVTHMDjEZz8MgguLL/Q7NblxhZUlTu9xSPqlsUO/y+X7dlU05jdhvyycD55DA==}
     engines: {node: ^18.0.0 || >=20.0.0}
     hasBin: true
@@ -17018,8 +17212,8 @@ packages:
       '@types/node': 20.12.7
       esbuild: 0.20.2
       postcss: 8.4.38
-      rollup: 4.14.1
-      sass: 1.74.1
+      rollup: 4.14.2
+      sass: 1.75.0
     optionalDependencies:
       fsevents: 2.3.3
     dev: true
@@ -17036,7 +17230,7 @@ packages:
     resolution: {integrity: sha512-AFbieoL7a5LMqcnOF04ji+rpXadgOXnZsxQr//r83kLPr7biP7am3g9zbaZIaBGwBRWeSvoMD4mgPdX3e4NWBg==}
     dev: true
 
-  /vue-draggable-plus@0.4.0(@types/sortablejs@1.15.4):
+  /vue-draggable-plus@0.4.0(@types/sortablejs@1.15.8):
     resolution: {integrity: sha512-CcvSopMmSZY9McCdQ56QsAydT5cZilx9LzLU0UIz6KDYe9ll6QnmNHN80t6wnxN402ZECSXc5X1dm/myiMFi+A==}
     peerDependencies:
       '@types/sortablejs': ^1.15.0
@@ -17045,17 +17239,17 @@ packages:
       '@vue/composition-api':
         optional: true
     dependencies:
-      '@types/sortablejs': 1.15.4
+      '@types/sortablejs': 1.15.8
     dev: true
 
-  /vue-eslint-parser@9.3.1(eslint@8.57.0):
+  /vue-eslint-parser@9.3.1(eslint@9.0.0):
     resolution: {integrity: sha512-Clr85iD2XFZ3lJ52/ppmUDG/spxQu6+MAeHXjjyI4I1NUYZ9xmenQp4N0oaHJhrA8OOxltCVxMRfANGa70vU0g==}
     engines: {node: ^14.17.0 || >=16.0.0}
     peerDependencies:
       eslint: '>=6.0.0'
     dependencies:
       debug: 4.3.4(supports-color@8.1.1)
-      eslint: 8.57.0
+      eslint: 9.0.0
       eslint-scope: 7.2.2
       eslint-visitor-keys: 3.4.3
       espree: 9.6.1
@@ -17066,14 +17260,14 @@ packages:
       - supports-color
     dev: true
 
-  /vue-eslint-parser@9.3.2(eslint@8.57.0):
+  /vue-eslint-parser@9.3.2(eslint@9.0.0):
     resolution: {integrity: sha512-q7tWyCVaV9f8iQyIA5Mkj/S6AoJ9KBN8IeUSf3XEmBrOtxOZnfTg5s4KClbZBCK3GtnT/+RyCLZyDHuZwTuBjg==}
     engines: {node: ^14.17.0 || >=16.0.0}
     peerDependencies:
       eslint: '>=6.0.0'
     dependencies:
       debug: 4.3.4(supports-color@8.1.1)
-      eslint: 8.57.0
+      eslint: 9.0.0
       eslint-scope: 7.2.2
       eslint-visitor-keys: 3.4.3
       espree: 9.6.1
@@ -17084,14 +17278,14 @@ packages:
       - supports-color
     dev: true
 
-  /vue-eslint-parser@9.4.2(eslint@8.57.0):
+  /vue-eslint-parser@9.4.2(eslint@9.0.0):
     resolution: {integrity: sha512-Ry9oiGmCAK91HrKMtCrKFWmSFWvYkpGglCeFAIqDdr9zdXmMMpJOmUJS7WWsW7fX81h6mwHmUZCQQ1E0PkSwYQ==}
     engines: {node: ^14.17.0 || >=16.0.0}
     peerDependencies:
       eslint: '>=6.0.0'
     dependencies:
       debug: 4.3.4(supports-color@8.1.1)
-      eslint: 8.57.0
+      eslint: 9.0.0
       eslint-scope: 7.2.2
       eslint-visitor-keys: 3.4.3
       espree: 9.6.1
@@ -17125,14 +17319,14 @@ packages:
       he: 1.2.0
     dev: true
 
-  /vue-tsc@2.0.12(typescript@5.4.5):
-    resolution: {integrity: sha512-thlBBWlPYrNdba535oDdxz7PRUufZgRZRVP5Aql5wBVpGSWSeqou4EzFXeKVoZr59lp9hJROubDVzlhACmcEhg==}
+  /vue-tsc@2.0.13(typescript@5.4.5):
+    resolution: {integrity: sha512-a3nL3FvguCWVJUQW/jFrUxdeUtiEkbZoQjidqvMeBK//tuE2w6NWQAbdrEpY2+6nSa4kZoKZp8TZUMtHpjt4mQ==}
     hasBin: true
     peerDependencies:
       typescript: '*'
     dependencies:
-      '@volar/typescript': 2.2.0-alpha.7
-      '@vue/language-core': 2.0.12(typescript@5.4.5)
+      '@volar/typescript': 2.2.0-alpha.8
+      '@vue/language-core': 2.0.13(typescript@5.4.5)
       semver: 7.6.0
       typescript: 5.4.5
     dev: true
@@ -17319,6 +17513,18 @@ packages:
       for-each: 0.3.3
       gopd: 1.0.1
       has-tostringtag: 1.0.0
+    dev: true
+
+  /which-typed-array@1.1.15:
+    resolution: {integrity: sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA==}
+    engines: {node: '>= 0.4'}
+    dependencies:
+      available-typed-arrays: 1.0.7
+      call-bind: 1.0.7
+      for-each: 0.3.3
+      gopd: 1.0.1
+      has-tostringtag: 1.0.2
+    dev: false
 
   /which@1.3.1:
     resolution: {integrity: sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==}
@@ -17459,7 +17665,7 @@ packages:
     resolution: {integrity: sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==}
     engines: {node: '>=4.0.0'}
     dependencies:
-      sax: 1.2.4
+      sax: 1.2.1
       xmlbuilder: 11.0.1
     dev: false
 
@@ -17677,9 +17883,9 @@ packages:
     name: plyr
     version: 3.7.0
     dependencies:
-      core-js: 3.33.0
+      core-js: 3.36.1
       custom-event-polyfill: 1.0.7
-      loadjs: 4.2.0
+      loadjs: 4.3.0
       rangetouch: 2.0.1
       url-polyfill: 1.1.12
     dev: true

From eb5dccacfe1a54c7d135c4a78e0df847573ec230 Mon Sep 17 00:00:00 2001
From: naskya <m@naskya.net>
Date: Mon, 15 Apr 2024 04:17:35 +0900
Subject: [PATCH 060/110] chore (firefish-js): remove bun lockfile

Why is it there?
---
 packages/firefish-js/bun.lockb | Bin 205415 -> 0 bytes
 1 file changed, 0 insertions(+), 0 deletions(-)
 delete mode 100755 packages/firefish-js/bun.lockb

diff --git a/packages/firefish-js/bun.lockb b/packages/firefish-js/bun.lockb
deleted file mode 100755
index 96788d2325e16f3b57147f261f42045089d2e48e..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001

literal 205415
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
CdwLH5


From f412d7ace3fcbabf2cf007358e1ebafe2aecffed Mon Sep 17 00:00:00 2001
From: naskya <m@naskya.net>
Date: Mon, 15 Apr 2024 04:30:49 +0900
Subject: [PATCH 061/110] chore (backend): remove 'quiet' settings

---
 packages/backend/src/boot/index.ts            |   4 +-
 packages/backend/src/boot/master.ts           |  98 ++++++------
 packages/backend/src/env.ts                   |   2 -
 .../remote/activitypub/kernel/delete/note.ts  |   4 +-
 .../api/endpoints/notes/make-private.ts       |   2 +-
 packages/backend/src/services/logger.ts       |   1 -
 packages/backend/src/services/note/delete.ts  | 142 +++++++++---------
 7 files changed, 119 insertions(+), 134 deletions(-)

diff --git a/packages/backend/src/boot/index.ts b/packages/backend/src/boot/index.ts
index 8caf7d062e..4ba24c280b 100644
--- a/packages/backend/src/boot/index.ts
+++ b/packages/backend/src/boot/index.ts
@@ -76,9 +76,7 @@ cluster.on("exit", (worker) => {
 });
 
 // Display detail of unhandled promise rejection
-if (!envOption.quiet) {
-	process.on("unhandledRejection", console.dir);
-}
+process.on("unhandledRejection", console.dir);
 
 // Display detail of uncaught exception
 process.on("uncaughtException", (err) => {
diff --git a/packages/backend/src/boot/master.ts b/packages/backend/src/boot/master.ts
index e54a2889e2..cd21c33021 100644
--- a/packages/backend/src/boot/master.ts
+++ b/packages/backend/src/boot/master.ts
@@ -28,58 +28,56 @@ const bootLogger = logger.createSubLogger("boot", "magenta", false);
 const themeColor = chalk.hex("#31748f");
 
 function greet() {
-	if (!envOption.quiet) {
-		//#region Firefish logo
-		console.log(
-			themeColor(
-				"██████╗ ██╗██████╗ ███████╗███████╗██╗███████╗██╗  ██╗    ○     ▄    ▄    ",
-			),
-		);
-		console.log(
-			themeColor(
-				"██╔════╝██║██╔══██╗██╔════╝██╔════╝██║██╔════╝██║  ██║      ⚬   █▄▄  █▄▄ ",
-			),
-		);
-		console.log(
-			themeColor(
-				"█████╗  ██║██████╔╝█████╗  █████╗  ██║███████╗███████║      ▄▄▄▄▄▄   ▄    ",
-			),
-		);
-		console.log(
-			themeColor(
-				"██╔══╝  ██║██╔══██╗██╔══╝  ██╔══╝  ██║╚════██║██╔══██║     █      █  █▄▄  ",
-			),
-		);
-		console.log(
-			themeColor(
-				"██║     ██║██║  ██║███████╗██║     ██║███████║██║  ██║     █ ● ●  █       ",
-			),
-		);
-		console.log(
-			themeColor(
-				"╚═╝     ╚═╝╚═╝  ╚═╝╚══════╝╚═╝     ╚═╝╚══════╝╚═╝  ╚═╝     ▀▄▄▄▄▄▄▀       ",
-			),
-		);
-		//#endregion
+	//#region Firefish logo
+	console.log(
+		themeColor(
+			"██████╗ ██╗██████╗ ███████╗███████╗██╗███████╗██╗  ██╗    ○     ▄    ▄    ",
+		),
+	);
+	console.log(
+		themeColor(
+			"██╔════╝██║██╔══██╗██╔════╝██╔════╝██║██╔════╝██║  ██║      ⚬   █▄▄  █▄▄ ",
+		),
+	);
+	console.log(
+		themeColor(
+			"█████╗  ██║██████╔╝█████╗  █████╗  ██║███████╗███████║      ▄▄▄▄▄▄   ▄    ",
+		),
+	);
+	console.log(
+		themeColor(
+			"██╔══╝  ██║██╔══██╗██╔══╝  ██╔══╝  ██║╚════██║██╔══██║     █      █  █▄▄  ",
+		),
+	);
+	console.log(
+		themeColor(
+			"██║     ██║██║  ██║███████╗██║     ██║███████║██║  ██║     █ ● ●  █       ",
+		),
+	);
+	console.log(
+		themeColor(
+			"╚═╝     ╚═╝╚═╝  ╚═╝╚══════╝╚═╝     ╚═╝╚══════╝╚═╝  ╚═╝     ▀▄▄▄▄▄▄▀       ",
+		),
+	);
+	//#endregion
 
-		console.log(
-			" Firefish is an open-source decentralized microblogging platform.",
-		);
-		console.log(
-			chalk.rgb(
-				255,
-				136,
-				0,
-			)(
-				" If you like Firefish, please consider contributing to the repo. https://firefish.dev/firefish/firefish",
-			),
-		);
+	console.log(
+		" Firefish is an open-source decentralized microblogging platform.",
+	);
+	console.log(
+		chalk.rgb(
+			255,
+			136,
+			0,
+		)(
+			" If you like Firefish, please consider contributing to the repo. https://firefish.dev/firefish/firefish",
+		),
+	);
 
-		console.log("");
-		console.log(
-			chalkTemplate`--- ${os.hostname()} {gray (PID: ${process.pid.toString()})} ---`,
-		);
-	}
+	console.log("");
+	console.log(
+		chalkTemplate`--- ${os.hostname()} {gray (PID: ${process.pid.toString()})} ---`,
+	);
 
 	bootLogger.info("Welcome to Firefish!");
 	bootLogger.info(`Firefish v${meta.version}`, null, true);
diff --git a/packages/backend/src/env.ts b/packages/backend/src/env.ts
index a788a0fba2..a10952133e 100644
--- a/packages/backend/src/env.ts
+++ b/packages/backend/src/env.ts
@@ -5,7 +5,6 @@ const envOption = {
 	disableClustering: false,
 	verbose: false,
 	withLogTime: false,
-	quiet: false,
 	slow: false,
 };
 
@@ -19,7 +18,6 @@ for (const key of Object.keys(envOption) as (keyof typeof envOption)[]) {
 }
 
 if (process.env.NODE_ENV === "test") envOption.disableClustering = true;
-if (process.env.NODE_ENV === "test") envOption.quiet = true;
 if (process.env.NODE_ENV === "test") envOption.noDaemons = true;
 
 export { envOption };
diff --git a/packages/backend/src/remote/activitypub/kernel/delete/note.ts b/packages/backend/src/remote/activitypub/kernel/delete/note.ts
index 4656480c2f..ae3a593d05 100644
--- a/packages/backend/src/remote/activitypub/kernel/delete/note.ts
+++ b/packages/backend/src/remote/activitypub/kernel/delete/note.ts
@@ -1,5 +1,5 @@
 import type { CacheableRemoteUser } from "@/models/entities/user.js";
-import deleteNode from "@/services/note/delete.js";
+import deleteNote from "@/services/note/delete.js";
 import { apLogger } from "../../logger.js";
 import DbResolver from "../../db-resolver.js";
 import { getApLock } from "@/misc/app-lock.js";
@@ -36,7 +36,7 @@ export default async function (
 			return "The user trying to delete the post is not the post author";
 		}
 
-		await deleteNode(actor, note);
+		await deleteNote(actor, note);
 		return "ok: note deleted";
 	} finally {
 		await lock.release();
diff --git a/packages/backend/src/server/api/endpoints/notes/make-private.ts b/packages/backend/src/server/api/endpoints/notes/make-private.ts
index 7b9ebc4d1a..5ddf1f3bf1 100644
--- a/packages/backend/src/server/api/endpoints/notes/make-private.ts
+++ b/packages/backend/src/server/api/endpoints/notes/make-private.ts
@@ -53,7 +53,7 @@ export default define(meta, paramDef, async (ps, user) => {
 		throw new ApiError(meta.errors.accessDenied);
 	}
 
-	await deleteNote(user, note, false, false);
+	await deleteNote(user, note, false);
 	await Notes.update(note.id, {
 		visibility: "specified",
 		visibleUserIds: [],
diff --git a/packages/backend/src/services/logger.ts b/packages/backend/src/services/logger.ts
index 63eb3d00b9..47a1fe82f8 100644
--- a/packages/backend/src/services/logger.ts
+++ b/packages/backend/src/services/logger.ts
@@ -56,7 +56,6 @@ export default class Logger {
 		subDomains: Domain[] = [],
 		store = true,
 	): void {
-		if (envOption.quiet) return;
 		if (
 			!(typeof config.logLevel === "undefined") &&
 			!config.logLevel.includes(level)
diff --git a/packages/backend/src/services/note/delete.ts b/packages/backend/src/services/note/delete.ts
index ac3515cfae..be3bf1e8b2 100644
--- a/packages/backend/src/services/note/delete.ts
+++ b/packages/backend/src/services/note/delete.ts
@@ -38,7 +38,6 @@ async function recalculateNotesCountOfLocalUser(user: {
 export default async function (
 	user: { id: User["id"]; uri: User["uri"]; host: User["host"] },
 	note: Note,
-	quiet = false,
 	deleteFromDb = true,
 ) {
 	const deletedAt = new Date();
@@ -67,87 +66,80 @@ export default async function (
 	}
 	const instanceNotesCountDecreasement: Record<string, number> = {};
 
-	if (!quiet) {
-		// Only broadcast "deleted" to local if the note is deleted from db
+	// Only broadcast "deleted" to local if the note is deleted from db
+	if (deleteFromDb) {
+		publishNoteStream(note.id, "deleted", {
+			deletedAt: deletedAt,
+		});
+	}
+
+	//#region ローカルの投稿なら削除アクティビティを配送
+	if (Users.isLocalUser(user) && !note.localOnly) {
+		let renote: Note | null = null;
+
+		// if deletd note is renote
+		if (
+			note.renoteId &&
+			note.text == null &&
+			!note.hasPoll &&
+			(note.fileIds == null || note.fileIds.length === 0)
+		) {
+			renote = await Notes.findOneBy({
+				id: note.renoteId,
+			});
+		}
+
+		const content = renderActivity(
+			renote
+				? renderUndo(
+						renderAnnounce(
+							renote.uri || `${config.url}/notes/${renote.id}`,
+							note,
+						),
+						user,
+					)
+				: renderDelete(renderTombstone(`${config.url}/notes/${note.id}`), user),
+		);
+
+		deliverToConcerned(user, note, content);
+	}
+
+	// also deliever delete activity to cascaded notes
+	for (const cascadingNote of cascadingNotes) {
 		if (deleteFromDb) {
-			publishNoteStream(note.id, "deleted", {
+			// For other notes, publishNoteStream is also required.
+			publishNoteStream(cascadingNote.id, "deleted", {
 				deletedAt: deletedAt,
 			});
 		}
 
-		//#region ローカルの投稿なら削除アクティビティを配送
-		if (Users.isLocalUser(user) && !note.localOnly) {
-			let renote: Note | null = null;
-
-			// if deletd note is renote
-			if (
-				note.renoteId &&
-				note.text == null &&
-				!note.hasPoll &&
-				(note.fileIds == null || note.fileIds.length === 0)
-			) {
-				renote = await Notes.findOneBy({
-					id: note.renoteId,
-				});
-			}
-
-			const content = renderActivity(
-				renote
-					? renderUndo(
-							renderAnnounce(
-								renote.uri || `${config.url}/notes/${renote.id}`,
-								note,
-							),
-							user,
-						)
-					: renderDelete(
-							renderTombstone(`${config.url}/notes/${note.id}`),
-							user,
-						),
-			);
-
-			deliverToConcerned(user, note, content);
+		if (!cascadingNote.user) continue;
+		if (!Users.isLocalUser(cascadingNote.user)) {
+			if (!Users.isRemoteUser(cascadingNote.user)) continue;
+			instanceNotesCountDecreasement[cascadingNote.user.host] ??= 0;
+			instanceNotesCountDecreasement[cascadingNote.user.host]++;
+			continue; // filter out remote users
 		}
+		affectedLocalUsers[cascadingNote.user.id] ??= cascadingNote.user;
+		if (cascadingNote.localOnly) continue; // filter out local-only notes
+		const content = renderActivity(
+			renderDelete(
+				renderTombstone(`${config.url}/notes/${cascadingNote.id}`),
+				cascadingNote.user,
+			),
+		);
+		deliverToConcerned(cascadingNote.user, cascadingNote, content);
+	}
+	//#endregion
 
-		// also deliever delete activity to cascaded notes
-		for (const cascadingNote of cascadingNotes) {
-			if (deleteFromDb) {
-				// For other notes, publishNoteStream is also required.
-				publishNoteStream(cascadingNote.id, "deleted", {
-					deletedAt: deletedAt,
-				});
-			}
-
-			if (!cascadingNote.user) continue;
-			if (!Users.isLocalUser(cascadingNote.user)) {
-				if (!Users.isRemoteUser(cascadingNote.user)) continue;
-				instanceNotesCountDecreasement[cascadingNote.user.host] ??= 0;
-				instanceNotesCountDecreasement[cascadingNote.user.host]++;
-				continue; // filter out remote users
-			}
-			affectedLocalUsers[cascadingNote.user.id] ??= cascadingNote.user;
-			if (cascadingNote.localOnly) continue; // filter out local-only notes
-			const content = renderActivity(
-				renderDelete(
-					renderTombstone(`${config.url}/notes/${cascadingNote.id}`),
-					cascadingNote.user,
-				),
-			);
-			deliverToConcerned(cascadingNote.user, cascadingNote, content);
-		}
-		//#endregion
-
-		if (Users.isRemoteUser(user)) {
-			instanceNotesCountDecreasement[user.host] ??= 0;
-			instanceNotesCountDecreasement[user.host]++;
-		}
-		for (const [host, count] of Object.entries(
-			instanceNotesCountDecreasement,
-		)) {
-			registerOrFetchInstanceDoc(host).then((i) => {
-				Instances.decrement({ id: i.id }, "notesCount", count);
-			});
-		}
+	if (Users.isRemoteUser(user)) {
+		instanceNotesCountDecreasement[user.host] ??= 0;
+		instanceNotesCountDecreasement[user.host]++;
+	}
+	for (const [host, count] of Object.entries(instanceNotesCountDecreasement)) {
+		registerOrFetchInstanceDoc(host).then((i) => {
+			Instances.decrement({ id: i.id }, "notesCount", count);
+		});
 	}
 
 	if (deleteFromDb) {

From 884c69f3777bfe26d5c519da102af450bcb6b65f Mon Sep 17 00:00:00 2001
From: naskya <m@naskya.net>
Date: Mon, 15 Apr 2024 04:34:00 +0900
Subject: [PATCH 062/110] chore (minor, backend): organize imports

---
 packages/backend/src/misc/reaction-lib.ts | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)

diff --git a/packages/backend/src/misc/reaction-lib.ts b/packages/backend/src/misc/reaction-lib.ts
index 691d9743c8..fbdfe949ff 100644
--- a/packages/backend/src/misc/reaction-lib.ts
+++ b/packages/backend/src/misc/reaction-lib.ts
@@ -1,7 +1,6 @@
 import { emojiRegex } from "./emoji-regex.js";
-import { fetchMeta } from "backend-rs";
+import { fetchMeta, toPuny } from "backend-rs";
 import { Emojis } from "@/models/index.js";
-import { toPuny } from "backend-rs";
 import { IsNull } from "typeorm";
 
 export function convertReactions(reactions: Record<string, number>) {

From 74875f174b1f84622ebb0375041483d2d04444e8 Mon Sep 17 00:00:00 2001
From: naskya <m@naskya.net>
Date: Mon, 15 Apr 2024 04:34:36 +0900
Subject: [PATCH 063/110] chore (minor, backend): use a template literal

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

diff --git a/packages/backend/src/misc/post.ts b/packages/backend/src/misc/post.ts
index dbe703d1a0..0b107ed009 100644
--- a/packages/backend/src/misc/post.ts
+++ b/packages/backend/src/misc/post.ts
@@ -12,7 +12,7 @@ export function parse(acct: any): Post {
 		cw: acct.cw,
 		localOnly: acct.localOnly,
 		createdAt: new Date(acct.createdAt),
-		visibility: "hidden" + (acct.visibility || ""),
+		visibility: `hidden${acct.visibility || ""}`,
 	};
 }
 

From 2731003bc948613e1161ea64219af6c361b75de1 Mon Sep 17 00:00:00 2001
From: naskya <m@naskya.net>
Date: Mon, 15 Apr 2024 05:13:35 +0900
Subject: [PATCH 064/110] refactor (backend): port emoji-regex to backend-rs

---
 Cargo.lock                                    |  34 ++
 Cargo.toml                                    |   1 +
 packages/backend-rs/Cargo.toml                |   1 +
 packages/backend-rs/index.d.ts                |   1 +
 packages/backend-rs/index.js                  |   3 +-
 packages/backend-rs/src/misc/emoji.rs         |  31 ++
 packages/backend-rs/src/misc/mod.rs           |   1 +
 packages/backend/package.json                 |   1 -
 packages/backend/src/misc/emoji-regex.ts      |   5 -
 packages/backend/src/misc/reaction-lib.ts     |  13 +-
 .../server/api/mastodon/endpoints/status.ts   |   5 +-
 pnpm-lock.yaml                                | 427 ++++++++----------
 12 files changed, 269 insertions(+), 254 deletions(-)
 create mode 100644 packages/backend-rs/src/misc/emoji.rs
 delete mode 100644 packages/backend/src/misc/emoji-regex.ts

diff --git a/Cargo.lock b/Cargo.lock
index bc584faf24..c6ba96e683 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -209,6 +209,7 @@ dependencies = [
  "cfg-if",
  "chrono",
  "cuid2",
+ "emojis",
  "idna",
  "jsonschema",
  "macro_rs",
@@ -702,6 +703,15 @@ dependencies = [
  "serde",
 ]
 
+[[package]]
+name = "emojis"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4ee61eb945bff65ee7d19d157d39c67c33290ff0742907413fd5eefd29edc979"
+dependencies = [
+ "phf",
+]
+
 [[package]]
 name = "encoding_rs"
 version = "0.8.34"
@@ -1662,6 +1672,24 @@ version = "2.3.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
 
+[[package]]
+name = "phf"
+version = "0.11.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc"
+dependencies = [
+ "phf_shared",
+]
+
+[[package]]
+name = "phf_shared"
+version = "0.11.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "90fcb95eef784c2ac79119d1dd819e162b5da872ce6f3c3abe1e8ca1c082f72b"
+dependencies = [
+ "siphasher",
+]
+
 [[package]]
 name = "pin-project-lite"
 version = "0.2.14"
@@ -2313,6 +2341,12 @@ version = "0.1.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "f27f6278552951f1f2b8cf9da965d10969b2efdea95a6ec47987ab46edfe263a"
 
+[[package]]
+name = "siphasher"
+version = "0.3.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d"
+
 [[package]]
 name = "slab"
 version = "0.4.9"
diff --git a/Cargo.toml b/Cargo.toml
index c9efe1f69a..7ca43c960b 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -17,6 +17,7 @@ cfg-if = "1.0.0"
 chrono = "0.4.37"
 convert_case = "0.6.0"
 cuid2 = "0.1.2"
+emojis = "0.6.1"
 idna = "0.5.0"
 jsonschema = "0.17.1"
 once_cell = "1.19.0"
diff --git a/packages/backend-rs/Cargo.toml b/packages/backend-rs/Cargo.toml
index 235fcc8706..af9e10cdc1 100644
--- a/packages/backend-rs/Cargo.toml
+++ b/packages/backend-rs/Cargo.toml
@@ -24,6 +24,7 @@ bcrypt = { workspace = true }
 cfg-if = { workspace = true }
 chrono = { workspace = true }
 cuid2 = { workspace = true }
+emojis = { workspace = true }
 idna = { workspace = true }
 jsonschema = { workspace = true }
 once_cell = { workspace = true }
diff --git a/packages/backend-rs/index.d.ts b/packages/backend-rs/index.d.ts
index a9398aacc1..9b8a64142f 100644
--- a/packages/backend-rs/index.d.ts
+++ b/packages/backend-rs/index.d.ts
@@ -132,6 +132,7 @@ export function isSelfHost(host?: string | undefined | null): boolean
 export function isSameOrigin(uri: string): boolean
 export function extractHost(uri: string): string
 export function toPuny(host: string): string
+export function isUnicodeEmoji(s: string): boolean
 export function sqlLikeEscape(src: string): string
 export function safeForSql(src: string): boolean
 /** Convert milliseconds to a human readable string */
diff --git a/packages/backend-rs/index.js b/packages/backend-rs/index.js
index 7a404d6447..16de16297f 100644
--- a/packages/backend-rs/index.js
+++ b/packages/backend-rs/index.js
@@ -310,7 +310,7 @@ if (!nativeBinding) {
   throw new Error(`Failed to load native binding`)
 }
 
-const { readServerConfig, stringToAcct, acctToString, checkWordMute, getFullApAccount, isSelfHost, isSameOrigin, extractHost, toPuny, sqlLikeEscape, safeForSql, formatMilliseconds, toMastodonId, fromMastodonId, fetchMeta, metaToPugArgs, nyaify, hashPassword, verifyPassword, isOldPasswordAlgorithm, AntennaSrcEnum, MutedNoteReasonEnum, NoteVisibilityEnum, NotificationTypeEnum, PageVisibilityEnum, PollNotevisibilityEnum, RelayStatusEnum, UserEmojimodpermEnum, UserProfileFfvisibilityEnum, UserProfileMutingnotificationtypesEnum, initIdGenerator, getTimestamp, genId, secureRndstr } = nativeBinding
+const { readServerConfig, stringToAcct, acctToString, checkWordMute, getFullApAccount, isSelfHost, isSameOrigin, extractHost, toPuny, isUnicodeEmoji, sqlLikeEscape, safeForSql, formatMilliseconds, toMastodonId, fromMastodonId, fetchMeta, metaToPugArgs, nyaify, hashPassword, verifyPassword, isOldPasswordAlgorithm, AntennaSrcEnum, MutedNoteReasonEnum, NoteVisibilityEnum, NotificationTypeEnum, PageVisibilityEnum, PollNotevisibilityEnum, RelayStatusEnum, UserEmojimodpermEnum, UserProfileFfvisibilityEnum, UserProfileMutingnotificationtypesEnum, initIdGenerator, getTimestamp, genId, secureRndstr } = nativeBinding
 
 module.exports.readServerConfig = readServerConfig
 module.exports.stringToAcct = stringToAcct
@@ -321,6 +321,7 @@ module.exports.isSelfHost = isSelfHost
 module.exports.isSameOrigin = isSameOrigin
 module.exports.extractHost = extractHost
 module.exports.toPuny = toPuny
+module.exports.isUnicodeEmoji = isUnicodeEmoji
 module.exports.sqlLikeEscape = sqlLikeEscape
 module.exports.safeForSql = safeForSql
 module.exports.formatMilliseconds = formatMilliseconds
diff --git a/packages/backend-rs/src/misc/emoji.rs b/packages/backend-rs/src/misc/emoji.rs
new file mode 100644
index 0000000000..df7d33848c
--- /dev/null
+++ b/packages/backend-rs/src/misc/emoji.rs
@@ -0,0 +1,31 @@
+#[inline]
+#[crate::export]
+pub fn is_unicode_emoji(s: &str) -> bool {
+    emojis::get(s).is_some()
+}
+
+#[cfg(test)]
+mod unit_test {
+    use super::is_unicode_emoji;
+
+    #[test]
+    fn test_unicode_emoji_check() {
+        assert!(is_unicode_emoji("⭐"));
+        assert!(is_unicode_emoji("👍"));
+        assert!(is_unicode_emoji("❤"));
+        assert!(is_unicode_emoji("♥️"));
+        assert!(is_unicode_emoji("❤️"));
+        assert!(is_unicode_emoji("💙"));
+        assert!(is_unicode_emoji("🩷"));
+        assert!(is_unicode_emoji("🖖🏿"));
+        assert!(is_unicode_emoji("🏃‍➡️"));
+        assert!(is_unicode_emoji("👩‍❤️‍👨"));
+        assert!(is_unicode_emoji("👩‍👦‍👦"));
+        assert!(is_unicode_emoji("🏳️‍🌈"));
+
+        assert!(!is_unicode_emoji("⭐⭐"));
+        assert!(!is_unicode_emoji("x"));
+        assert!(!is_unicode_emoji("\t"));
+        assert!(!is_unicode_emoji(":meow_aww:"));
+    }
+}
diff --git a/packages/backend-rs/src/misc/mod.rs b/packages/backend-rs/src/misc/mod.rs
index 74a483ea51..3fd447f4d5 100644
--- a/packages/backend-rs/src/misc/mod.rs
+++ b/packages/backend-rs/src/misc/mod.rs
@@ -1,6 +1,7 @@
 pub mod acct;
 pub mod check_word_mute;
 pub mod convert_host;
+pub mod emoji;
 pub mod escape_sql;
 pub mod format_milliseconds;
 pub mod mastodon_id;
diff --git a/packages/backend/package.json b/packages/backend/package.json
index bf435ec64b..9289c2f7ea 100644
--- a/packages/backend/package.json
+++ b/packages/backend/package.json
@@ -33,7 +33,6 @@
 		"@peertube/http-signature": "1.7.0",
 		"@redocly/openapi-core": "1.11.0",
 		"@sinonjs/fake-timers": "11.2.2",
-		"@twemoji/parser": "^15.1.1",
 		"adm-zip": "0.5.10",
 		"ajv": "8.12.0",
 		"archiver": "7.0.1",
diff --git a/packages/backend/src/misc/emoji-regex.ts b/packages/backend/src/misc/emoji-regex.ts
deleted file mode 100644
index 72d6a62d9a..0000000000
--- a/packages/backend/src/misc/emoji-regex.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-import twemoji from "@twemoji/parser/dist/lib/regex.js";
-const twemojiRegex = twemoji.default;
-
-export const emojiRegex = new RegExp(`(${twemojiRegex.source})`);
-export const emojiRegexAtStartToEnd = new RegExp(`^(${twemojiRegex.source})$`);
diff --git a/packages/backend/src/misc/reaction-lib.ts b/packages/backend/src/misc/reaction-lib.ts
index fbdfe949ff..ac9ba5652d 100644
--- a/packages/backend/src/misc/reaction-lib.ts
+++ b/packages/backend/src/misc/reaction-lib.ts
@@ -1,5 +1,4 @@
-import { emojiRegex } from "./emoji-regex.js";
-import { fetchMeta, toPuny } from "backend-rs";
+import { fetchMeta, isUnicodeEmoji, toPuny } from "backend-rs";
 import { Emojis } from "@/models/index.js";
 import { IsNull } from "typeorm";
 
@@ -22,17 +21,15 @@ export async function toDbReaction(
 ): Promise<string> {
 	if (!reaction) return (await fetchMeta(true)).defaultReaction;
 
-	reacterHost = reacterHost == null ? null : toPuny(reacterHost);
-
 	if (reaction.includes("❤") || reaction.includes("♥️")) return "❤️";
 
 	// Allow unicode reactions
-	const match = emojiRegex.exec(reaction);
-	if (match) {
-		const unicode = match[0];
-		return unicode;
+	if (isUnicodeEmoji(reaction)) {
+		return reaction;
 	}
 
+	reacterHost = reacterHost == null ? null : toPuny(reacterHost);
+
 	const custom = reaction.match(/^:([\w+-]+)(?:@\.)?:$/);
 	if (custom) {
 		const name = custom[1];
diff --git a/packages/backend/src/server/api/mastodon/endpoints/status.ts b/packages/backend/src/server/api/mastodon/endpoints/status.ts
index 5286c90fac..6fa70717e7 100644
--- a/packages/backend/src/server/api/mastodon/endpoints/status.ts
+++ b/packages/backend/src/server/api/mastodon/endpoints/status.ts
@@ -1,10 +1,9 @@
 import Router from "@koa/router";
 import { getClient } from "../ApiMastodonCompatibleService.js";
-import { emojiRegexAtStartToEnd } from "@/misc/emoji-regex.js";
 import querystring from "node:querystring";
 import qs from "qs";
 import { convertTimelinesArgsId, limitToInt } from "./timeline.js";
-import { fetchMeta, fromMastodonId } from "backend-rs";
+import { fetchMeta, fromMastodonId, isUnicodeEmoji } from "backend-rs";
 import {
 	convertAccount,
 	convertAttachment,
@@ -37,7 +36,7 @@ export function apiStatusMastodon(router: Router): void {
 			}
 			const text = body.status;
 			const removed = text.replace(/@\S+/g, "").replace(/\s|​/g, "");
-			const isDefaultEmoji = emojiRegexAtStartToEnd.test(removed);
+			const isDefaultEmoji = isUnicodeEmoji(removed);
 			const isCustomEmoji = /^:[a-zA-Z0-9@_]+:$/.test(removed);
 			if ((body.in_reply_to_id && isDefaultEmoji) || isCustomEmoji) {
 				const a = await client.createEmojiReaction(
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 53b6089f9c..b591ffe9c1 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -87,9 +87,6 @@ importers:
       '@sinonjs/fake-timers':
         specifier: 11.2.2
         version: 11.2.2
-      '@twemoji/parser':
-        specifier: ^15.1.1
-        version: 15.1.1
       adm-zip:
         specifier: 0.5.10
         version: 0.5.10
@@ -547,10 +544,10 @@ importers:
     devDependencies:
       '@eslint-sets/eslint-config-vue3':
         specifier: ^5.12.0
-        version: 5.12.0(@babel/core@7.24.4)(eslint@9.0.0)(prettier@3.2.5)(typescript@5.4.5)
+        version: 5.12.0(@babel/core@7.24.4)(eslint@8.46.0)(prettier@3.2.5)(typescript@5.4.5)
       '@eslint-sets/eslint-config-vue3-ts':
         specifier: ^3.3.0
-        version: 3.3.0(@babel/core@7.24.4)(eslint@9.0.0)(prettier@3.2.5)(typescript@5.4.5)
+        version: 3.3.0(@babel/core@7.24.4)(eslint@8.46.0)(prettier@3.2.5)(typescript@5.4.5)
       '@phosphor-icons/web':
         specifier: ^2.1.1
         version: 2.1.1
@@ -658,7 +655,7 @@ importers:
         version: 3.0.12
       eslint-plugin-file-progress:
         specifier: ^1.3.0
-        version: 1.3.0(eslint@9.0.0)
+        version: 1.3.0(eslint@8.46.0)
       eventemitter3:
         specifier: 5.0.1
         version: 5.0.1
@@ -1081,7 +1078,7 @@ packages:
       - supports-color
     dev: true
 
-  /@babel/eslint-parser@7.23.10(@babel/core@7.24.4)(eslint@9.0.0):
+  /@babel/eslint-parser@7.23.10(@babel/core@7.24.4)(eslint@8.46.0):
     resolution: {integrity: sha512-3wSYDPZVnhseRnxRJH6ZVTNknBz76AEnyC+AYYhasjP3Yy23qz0ERR7Fcd2SHmYuSFJ2kY9gaaDd3vyqU09eSw==}
     engines: {node: ^10.13.0 || ^12.13.0 || >=14.0.0}
     peerDependencies:
@@ -1090,12 +1087,12 @@ packages:
     dependencies:
       '@babel/core': 7.24.4
       '@nicolo-ribaudo/eslint-scope-5-internals': 5.1.1-v1
-      eslint: 9.0.0
+      eslint: 8.46.0
       eslint-visitor-keys: 2.1.0
       semver: 6.3.1
     dev: true
 
-  /@babel/eslint-parser@7.23.3(@babel/core@7.24.4)(eslint@9.0.0):
+  /@babel/eslint-parser@7.23.3(@babel/core@7.24.4)(eslint@8.46.0):
     resolution: {integrity: sha512-9bTuNlyx7oSstodm1cR1bECj4fkiknsDa1YniISkJemMY3DGhJNYBECbe6QD/q54mp2J8VO66jW3/7uP//iFCw==}
     engines: {node: ^10.13.0 || ^12.13.0 || >=14.0.0}
     peerDependencies:
@@ -1104,7 +1101,7 @@ packages:
     dependencies:
       '@babel/core': 7.24.4
       '@nicolo-ribaudo/eslint-scope-5-internals': 5.1.1-v1
-      eslint: 9.0.0
+      eslint: 8.46.0
       eslint-visitor-keys: 2.1.0
       semver: 6.3.1
     dev: true
@@ -2256,29 +2253,29 @@ packages:
     engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0}
     dev: true
 
-  /@eslint-sets/eslint-config-basic@3.3.0(@babel/core@7.24.4)(@typescript-eslint/parser@5.62.0)(eslint@9.0.0)(prettier@3.2.5):
+  /@eslint-sets/eslint-config-basic@3.3.0(@babel/core@7.24.4)(@typescript-eslint/parser@5.62.0)(eslint@8.46.0)(prettier@3.2.5):
     resolution: {integrity: sha512-x5YH0CvZJxn19/5ehu188XaoLQpxOGlFiIuPHCN6FyONgrmriakT/cmIIBOJg2Vi/y1bn2xbhsgVNb00J3HyTg==}
     peerDependencies:
       eslint: '>=8.0.0'
       prettier: '>=2.0.0'
     dependencies:
-      '@babel/eslint-parser': 7.23.3(@babel/core@7.24.4)(eslint@9.0.0)
-      eslint: 9.0.0
-      eslint-config-prettier: 8.9.0(eslint@9.0.0)
-      eslint-plugin-eslint-comments: 3.2.0(eslint@9.0.0)
+      '@babel/eslint-parser': 7.23.3(@babel/core@7.24.4)(eslint@8.46.0)
+      eslint: 8.46.0
+      eslint-config-prettier: 8.9.0(eslint@8.46.0)
+      eslint-plugin-eslint-comments: 3.2.0(eslint@8.46.0)
       eslint-plugin-html: 7.1.0
-      eslint-plugin-import: 2.29.0(@typescript-eslint/parser@5.62.0)(eslint@9.0.0)
-      eslint-plugin-jsonc: 2.10.0(eslint@9.0.0)
-      eslint-plugin-markdown: 3.0.1(eslint@9.0.0)
-      eslint-plugin-n: 15.7.0(eslint@9.0.0)
-      eslint-plugin-prettier: 4.2.1(eslint-config-prettier@8.9.0)(eslint@9.0.0)(prettier@3.2.5)
-      eslint-plugin-promise: 5.2.0(eslint@9.0.0)
+      eslint-plugin-import: 2.29.0(@typescript-eslint/parser@5.62.0)(eslint@8.46.0)
+      eslint-plugin-jsonc: 2.10.0(eslint@8.46.0)
+      eslint-plugin-markdown: 3.0.1(eslint@8.46.0)
+      eslint-plugin-n: 15.7.0(eslint@8.46.0)
+      eslint-plugin-prettier: 4.2.1(eslint-config-prettier@8.9.0)(eslint@8.46.0)(prettier@3.2.5)
+      eslint-plugin-promise: 5.2.0(eslint@8.46.0)
       eslint-plugin-tsdoc: 0.2.17
-      eslint-plugin-unicorn: 45.0.2(eslint@9.0.0)
-      eslint-plugin-yml: 1.10.0(eslint@9.0.0)
+      eslint-plugin-unicorn: 45.0.2(eslint@8.46.0)
+      eslint-plugin-yml: 1.10.0(eslint@8.46.0)
       jsonc-eslint-parser: 2.3.0
       prettier: 3.2.5
-      vue-eslint-parser: 9.3.2(eslint@9.0.0)
+      vue-eslint-parser: 9.3.2(eslint@8.46.0)
       yaml-eslint-parser: 1.2.2
     transitivePeerDependencies:
       - '@babel/core'
@@ -2288,7 +2285,7 @@ packages:
       - supports-color
     dev: true
 
-  /@eslint-sets/eslint-config-basic@5.12.0(@babel/core@7.24.4)(@typescript-eslint/parser@6.21.0)(eslint@9.0.0)(prettier@3.2.5)(typescript@5.4.5):
+  /@eslint-sets/eslint-config-basic@5.12.0(@babel/core@7.24.4)(@typescript-eslint/parser@6.21.0)(eslint@8.46.0)(prettier@3.2.5)(typescript@5.4.5):
     resolution: {integrity: sha512-AgECfmJsiVOWKmvgjv780VuuoT9SE6PRgxGTtytHSfE9b9MAJjHxToVTKtD4UEKvocEGbg2EcwqGbff8cxDWKw==}
     peerDependencies:
       eslint: '>=7.4.0'
@@ -2298,24 +2295,24 @@ packages:
       typescript:
         optional: true
     dependencies:
-      '@babel/eslint-parser': 7.23.10(@babel/core@7.24.4)(eslint@9.0.0)
-      eslint: 9.0.0
-      eslint-config-prettier: 9.1.0(eslint@9.0.0)
-      eslint-plugin-eslint-comments: 3.2.0(eslint@9.0.0)
+      '@babel/eslint-parser': 7.23.10(@babel/core@7.24.4)(eslint@8.46.0)
+      eslint: 8.46.0
+      eslint-config-prettier: 9.1.0(eslint@8.46.0)
+      eslint-plugin-eslint-comments: 3.2.0(eslint@8.46.0)
       eslint-plugin-html: 7.1.0
-      eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.21.0)(eslint@9.0.0)
-      eslint-plugin-jsonc: 2.13.0(eslint@9.0.0)
-      eslint-plugin-markdown: 3.0.1(eslint@9.0.0)
-      eslint-plugin-n: 16.6.2(eslint@9.0.0)
-      eslint-plugin-prettier: 5.1.3(eslint-config-prettier@9.1.0)(eslint@9.0.0)(prettier@3.2.5)
-      eslint-plugin-promise: 6.1.1(eslint@9.0.0)
+      eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.21.0)(eslint@8.46.0)
+      eslint-plugin-jsonc: 2.13.0(eslint@8.46.0)
+      eslint-plugin-markdown: 3.0.1(eslint@8.46.0)
+      eslint-plugin-n: 16.6.2(eslint@8.46.0)
+      eslint-plugin-prettier: 5.1.3(eslint-config-prettier@9.1.0)(eslint@8.46.0)(prettier@3.2.5)
+      eslint-plugin-promise: 6.1.1(eslint@8.46.0)
       eslint-plugin-tsdoc: 0.2.17
-      eslint-plugin-unicorn: 40.1.0(eslint@9.0.0)
-      eslint-plugin-yml: 1.12.2(eslint@9.0.0)
+      eslint-plugin-unicorn: 40.1.0(eslint@8.46.0)
+      eslint-plugin-yml: 1.12.2(eslint@8.46.0)
       jsonc-eslint-parser: 2.4.0
       prettier: 3.2.5
       typescript: 5.4.5
-      vue-eslint-parser: 9.4.2(eslint@9.0.0)
+      vue-eslint-parser: 9.4.2(eslint@8.46.0)
       yaml-eslint-parser: 1.2.2
     transitivePeerDependencies:
       - '@babel/core'
@@ -2326,19 +2323,19 @@ packages:
       - supports-color
     dev: true
 
-  /@eslint-sets/eslint-config-ts@3.3.0(@babel/core@7.24.4)(eslint@9.0.0)(prettier@3.2.5)(typescript@5.4.5):
+  /@eslint-sets/eslint-config-ts@3.3.0(@babel/core@7.24.4)(eslint@8.46.0)(prettier@3.2.5)(typescript@5.4.5):
     resolution: {integrity: sha512-4Vj3KxYx16hmW6AyEv1mil0gVN8H3rdJt8TRWufbAj0ZN+EjwOPf3TqE7ASCYto/NpA8xWQY3NGm/og9Or/dDQ==}
     peerDependencies:
       eslint: '>=8.0.0'
       prettier: '>=2.0.0'
       typescript: '>=4.0.0'
     dependencies:
-      '@eslint-sets/eslint-config-basic': 3.3.0(@babel/core@7.24.4)(@typescript-eslint/parser@5.62.0)(eslint@9.0.0)(prettier@3.2.5)
-      '@typescript-eslint/eslint-plugin': 5.62.0(@typescript-eslint/parser@5.62.0)(eslint@9.0.0)(typescript@5.4.5)
-      '@typescript-eslint/parser': 5.62.0(eslint@9.0.0)(typescript@5.4.5)
-      eslint: 9.0.0
-      eslint-config-prettier: 8.9.0(eslint@9.0.0)
-      eslint-plugin-prettier: 4.2.1(eslint-config-prettier@8.9.0)(eslint@9.0.0)(prettier@3.2.5)
+      '@eslint-sets/eslint-config-basic': 3.3.0(@babel/core@7.24.4)(@typescript-eslint/parser@5.62.0)(eslint@8.46.0)(prettier@3.2.5)
+      '@typescript-eslint/eslint-plugin': 5.62.0(@typescript-eslint/parser@5.62.0)(eslint@8.46.0)(typescript@5.4.5)
+      '@typescript-eslint/parser': 5.62.0(eslint@8.46.0)(typescript@5.4.5)
+      eslint: 8.46.0
+      eslint-config-prettier: 8.9.0(eslint@8.46.0)
+      eslint-plugin-prettier: 4.2.1(eslint-config-prettier@8.9.0)(eslint@8.46.0)(prettier@3.2.5)
       eslint-plugin-tsdoc: 0.2.17
       prettier: 3.2.5
       typescript: 5.4.5
@@ -2349,7 +2346,7 @@ packages:
       - supports-color
     dev: true
 
-  /@eslint-sets/eslint-config-ts@5.12.0(@babel/core@7.24.4)(eslint@9.0.0)(prettier@3.2.5)(typescript@5.4.5):
+  /@eslint-sets/eslint-config-ts@5.12.0(@babel/core@7.24.4)(eslint@8.46.0)(prettier@3.2.5)(typescript@5.4.5):
     resolution: {integrity: sha512-7vOzV6qYv0SbA9W17m9lkG/Zv+qVeCcAbWEY1d9hUbBHx9Ip48kNMNVDrnh97zUORXGcmjxsZ81W2lC36Ox2pw==}
     peerDependencies:
       eslint: '>=7.4.0'
@@ -2359,12 +2356,12 @@ packages:
       typescript:
         optional: true
     dependencies:
-      '@eslint-sets/eslint-config-basic': 5.12.0(@babel/core@7.24.4)(@typescript-eslint/parser@6.21.0)(eslint@9.0.0)(prettier@3.2.5)(typescript@5.4.5)
-      '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0)(eslint@9.0.0)(typescript@5.4.5)
-      '@typescript-eslint/parser': 6.21.0(eslint@9.0.0)(typescript@5.4.5)
-      eslint: 9.0.0
-      eslint-config-prettier: 9.1.0(eslint@9.0.0)
-      eslint-plugin-prettier: 5.1.3(eslint-config-prettier@9.1.0)(eslint@9.0.0)(prettier@3.2.5)
+      '@eslint-sets/eslint-config-basic': 5.12.0(@babel/core@7.24.4)(@typescript-eslint/parser@6.21.0)(eslint@8.46.0)(prettier@3.2.5)(typescript@5.4.5)
+      '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0)(eslint@8.46.0)(typescript@5.4.5)
+      '@typescript-eslint/parser': 6.21.0(eslint@8.46.0)(typescript@5.4.5)
+      eslint: 8.46.0
+      eslint-config-prettier: 9.1.0(eslint@8.46.0)
+      eslint-plugin-prettier: 5.1.3(eslint-config-prettier@9.1.0)(eslint@8.46.0)(prettier@3.2.5)
       eslint-plugin-tsdoc: 0.2.17
       prettier: 3.2.5
       typescript: 5.4.5
@@ -2376,26 +2373,26 @@ packages:
       - supports-color
     dev: true
 
-  /@eslint-sets/eslint-config-vue3-ts@3.3.0(@babel/core@7.24.4)(eslint@9.0.0)(prettier@3.2.5)(typescript@5.4.5):
+  /@eslint-sets/eslint-config-vue3-ts@3.3.0(@babel/core@7.24.4)(eslint@8.46.0)(prettier@3.2.5)(typescript@5.4.5):
     resolution: {integrity: sha512-KX3VFuS5U4FYKfZ6PABQjl54BMpNapNjYYe103Nm2Zy8y9zphDCBAARbhU97XNSvzkurve7HhJcsi9gXrWlGFA==}
     peerDependencies:
       eslint: '>=8.0.0'
       prettier: '>=2.0.0'
       typescript: '>=4.0.0'
     dependencies:
-      '@eslint-sets/eslint-config-ts': 3.3.0(@babel/core@7.24.4)(eslint@9.0.0)(prettier@3.2.5)(typescript@5.4.5)
-      '@typescript-eslint/eslint-plugin': 5.62.0(@typescript-eslint/parser@5.62.0)(eslint@9.0.0)(typescript@5.4.5)
-      '@typescript-eslint/parser': 5.62.0(eslint@9.0.0)(typescript@5.4.5)
-      eslint: 9.0.0
-      eslint-config-prettier: 8.9.0(eslint@9.0.0)
-      eslint-plugin-prettier: 4.2.1(eslint-config-prettier@8.9.0)(eslint@9.0.0)(prettier@3.2.5)
+      '@eslint-sets/eslint-config-ts': 3.3.0(@babel/core@7.24.4)(eslint@8.46.0)(prettier@3.2.5)(typescript@5.4.5)
+      '@typescript-eslint/eslint-plugin': 5.62.0(@typescript-eslint/parser@5.62.0)(eslint@8.46.0)(typescript@5.4.5)
+      '@typescript-eslint/parser': 5.62.0(eslint@8.46.0)(typescript@5.4.5)
+      eslint: 8.46.0
+      eslint-config-prettier: 8.9.0(eslint@8.46.0)
+      eslint-plugin-prettier: 4.2.1(eslint-config-prettier@8.9.0)(eslint@8.46.0)(prettier@3.2.5)
       eslint-plugin-tsdoc: 0.2.17
       eslint-plugin-vitest-globals: 1.4.0
-      eslint-plugin-vue: 9.16.1(eslint@9.0.0)
-      eslint-plugin-vue-scoped-css: 2.5.0(eslint@9.0.0)(vue-eslint-parser@9.3.1)
+      eslint-plugin-vue: 9.16.1(eslint@8.46.0)
+      eslint-plugin-vue-scoped-css: 2.5.0(eslint@8.46.0)(vue-eslint-parser@9.3.1)
       prettier: 3.2.5
       typescript: 5.4.5
-      vue-eslint-parser: 9.3.1(eslint@9.0.0)
+      vue-eslint-parser: 9.3.1(eslint@8.46.0)
     transitivePeerDependencies:
       - '@babel/core'
       - eslint-import-resolver-typescript
@@ -2403,7 +2400,7 @@ packages:
       - supports-color
     dev: true
 
-  /@eslint-sets/eslint-config-vue3@5.12.0(@babel/core@7.24.4)(eslint@9.0.0)(prettier@3.2.5)(typescript@5.4.5):
+  /@eslint-sets/eslint-config-vue3@5.12.0(@babel/core@7.24.4)(eslint@8.46.0)(prettier@3.2.5)(typescript@5.4.5):
     resolution: {integrity: sha512-gQBmQicZihPcxncIdkKagQGZ2dH+97ioAlUpsaczEdgY9pLrLOU5oGTetjbaxAp6zGS2sXm1n0i2BnwRIlt4Bg==}
     peerDependencies:
       eslint: '>=7.4.0'
@@ -2413,22 +2410,22 @@ packages:
       typescript:
         optional: true
     dependencies:
-      '@eslint-sets/eslint-config-basic': 5.12.0(@babel/core@7.24.4)(@typescript-eslint/parser@6.21.0)(eslint@9.0.0)(prettier@3.2.5)(typescript@5.4.5)
-      '@eslint-sets/eslint-config-ts': 5.12.0(@babel/core@7.24.4)(eslint@9.0.0)(prettier@3.2.5)(typescript@5.4.5)
-      '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0)(eslint@9.0.0)(typescript@5.4.5)
-      '@typescript-eslint/parser': 6.21.0(eslint@9.0.0)(typescript@5.4.5)
-      eslint: 9.0.0
-      eslint-config-prettier: 9.1.0(eslint@9.0.0)
-      eslint-plugin-jsdoc: 48.0.6(eslint@9.0.0)
-      eslint-plugin-prettier: 5.1.3(eslint-config-prettier@9.1.0)(eslint@9.0.0)(prettier@3.2.5)
+      '@eslint-sets/eslint-config-basic': 5.12.0(@babel/core@7.24.4)(@typescript-eslint/parser@6.21.0)(eslint@8.46.0)(prettier@3.2.5)(typescript@5.4.5)
+      '@eslint-sets/eslint-config-ts': 5.12.0(@babel/core@7.24.4)(eslint@8.46.0)(prettier@3.2.5)(typescript@5.4.5)
+      '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0)(eslint@8.46.0)(typescript@5.4.5)
+      '@typescript-eslint/parser': 6.21.0(eslint@8.46.0)(typescript@5.4.5)
+      eslint: 8.46.0
+      eslint-config-prettier: 9.1.0(eslint@8.46.0)
+      eslint-plugin-jsdoc: 48.0.6(eslint@8.46.0)
+      eslint-plugin-prettier: 5.1.3(eslint-config-prettier@9.1.0)(eslint@8.46.0)(prettier@3.2.5)
       eslint-plugin-tsdoc: 0.2.17
       eslint-plugin-vitest-globals: 1.4.0
-      eslint-plugin-vue: 9.21.1(eslint@9.0.0)
-      eslint-plugin-vue-scoped-css: 2.7.2(eslint@9.0.0)(vue-eslint-parser@9.4.2)
+      eslint-plugin-vue: 9.21.1(eslint@8.46.0)
+      eslint-plugin-vue-scoped-css: 2.7.2(eslint@8.46.0)(vue-eslint-parser@9.4.2)
       local-pkg: 0.5.0
       prettier: 3.2.5
       typescript: 5.4.5
-      vue-eslint-parser: 9.4.2(eslint@9.0.0)
+      vue-eslint-parser: 9.4.2(eslint@8.46.0)
     transitivePeerDependencies:
       - '@babel/core'
       - '@types/eslint'
@@ -3880,10 +3877,6 @@ packages:
   /@twemoji/parser@15.0.0:
     resolution: {integrity: sha512-lh9515BNsvKSNvyUqbj5yFu83iIDQ77SwVcsN/SnEGawczhsKU6qWuogewN1GweTi5Imo5ToQ9s+nNTf97IXvg==}
 
-  /@twemoji/parser@15.1.1:
-    resolution: {integrity: sha512-CChRzIu6ngkCJOmURBlYEdX5DZSu+bBTtqR60XjBkFrmvplKW7OQsea+i8XwF4bLVlUXBO7ZmHhRPDzfQyLwwg==}
-    dev: false
-
   /@types/accepts@1.3.5:
     resolution: {integrity: sha512-jOdnI/3qTpHABjM5cx1Hc0sKsPoYCp+DP/GJRGtDlPd7fiV9oXGGIcjW/ZOxLIvjGz8MA+uMZI9metHlgqbgwQ==}
     dependencies:
@@ -4579,7 +4572,7 @@ packages:
       - supports-color
     dev: true
 
-  /@typescript-eslint/eslint-plugin@5.62.0(@typescript-eslint/parser@5.62.0)(eslint@9.0.0)(typescript@5.4.5):
+  /@typescript-eslint/eslint-plugin@5.62.0(@typescript-eslint/parser@5.62.0)(eslint@8.46.0)(typescript@5.4.5):
     resolution: {integrity: sha512-TiZzBSJja/LbhNPvk6yc0JrX9XqhQ0hdh6M2svYfsHGejaKFIAGd9MQ+ERIMzLGlN/kZoYIgdxFV0PuljTKXag==}
     engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
     peerDependencies:
@@ -4591,12 +4584,12 @@ packages:
         optional: true
     dependencies:
       '@eslint-community/regexpp': 4.9.1
-      '@typescript-eslint/parser': 5.62.0(eslint@9.0.0)(typescript@5.4.5)
+      '@typescript-eslint/parser': 5.62.0(eslint@8.46.0)(typescript@5.4.5)
       '@typescript-eslint/scope-manager': 5.62.0
-      '@typescript-eslint/type-utils': 5.62.0(eslint@9.0.0)(typescript@5.4.5)
-      '@typescript-eslint/utils': 5.62.0(eslint@9.0.0)(typescript@5.4.5)
+      '@typescript-eslint/type-utils': 5.62.0(eslint@8.46.0)(typescript@5.4.5)
+      '@typescript-eslint/utils': 5.62.0(eslint@8.46.0)(typescript@5.4.5)
       debug: 4.3.4(supports-color@8.1.1)
-      eslint: 9.0.0
+      eslint: 8.46.0
       graphemer: 1.4.0
       ignore: 5.2.4
       natural-compare-lite: 1.4.0
@@ -4607,7 +4600,7 @@ packages:
       - supports-color
     dev: true
 
-  /@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0)(eslint@9.0.0)(typescript@5.4.5):
+  /@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0)(eslint@8.46.0)(typescript@5.4.5):
     resolution: {integrity: sha512-oy9+hTPCUFpngkEZUSzbf9MxI65wbKFoQYsgPdILTfbUldp5ovUuphZVe4i30emU9M/kP+T64Di0mxl7dSw3MA==}
     engines: {node: ^16.0.0 || >=18.0.0}
     peerDependencies:
@@ -4619,13 +4612,13 @@ packages:
         optional: true
     dependencies:
       '@eslint-community/regexpp': 4.9.1
-      '@typescript-eslint/parser': 6.21.0(eslint@9.0.0)(typescript@5.4.5)
+      '@typescript-eslint/parser': 6.21.0(eslint@8.46.0)(typescript@5.4.5)
       '@typescript-eslint/scope-manager': 6.21.0
-      '@typescript-eslint/type-utils': 6.21.0(eslint@9.0.0)(typescript@5.4.5)
-      '@typescript-eslint/utils': 6.21.0(eslint@9.0.0)(typescript@5.4.5)
+      '@typescript-eslint/type-utils': 6.21.0(eslint@8.46.0)(typescript@5.4.5)
+      '@typescript-eslint/utils': 6.21.0(eslint@8.46.0)(typescript@5.4.5)
       '@typescript-eslint/visitor-keys': 6.21.0
       debug: 4.3.4(supports-color@8.1.1)
-      eslint: 9.0.0
+      eslint: 8.46.0
       graphemer: 1.4.0
       ignore: 5.2.4
       natural-compare: 1.4.0
@@ -4656,7 +4649,7 @@ packages:
       - supports-color
     dev: true
 
-  /@typescript-eslint/parser@5.62.0(eslint@9.0.0)(typescript@5.4.5):
+  /@typescript-eslint/parser@5.62.0(eslint@8.46.0)(typescript@5.4.5):
     resolution: {integrity: sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==}
     engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
     peerDependencies:
@@ -4670,13 +4663,13 @@ packages:
       '@typescript-eslint/types': 5.62.0
       '@typescript-eslint/typescript-estree': 5.62.0(typescript@5.4.5)
       debug: 4.3.4(supports-color@8.1.1)
-      eslint: 9.0.0
+      eslint: 8.46.0
       typescript: 5.4.5
     transitivePeerDependencies:
       - supports-color
     dev: true
 
-  /@typescript-eslint/parser@6.21.0(eslint@9.0.0)(typescript@5.4.5):
+  /@typescript-eslint/parser@6.21.0(eslint@8.46.0)(typescript@5.4.5):
     resolution: {integrity: sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==}
     engines: {node: ^16.0.0 || >=18.0.0}
     peerDependencies:
@@ -4691,7 +4684,7 @@ packages:
       '@typescript-eslint/typescript-estree': 6.21.0(typescript@5.4.5)
       '@typescript-eslint/visitor-keys': 6.21.0
       debug: 4.3.4(supports-color@8.1.1)
-      eslint: 9.0.0
+      eslint: 8.46.0
       typescript: 5.4.5
     transitivePeerDependencies:
       - supports-color
@@ -4733,7 +4726,7 @@ packages:
       - supports-color
     dev: true
 
-  /@typescript-eslint/type-utils@5.62.0(eslint@9.0.0)(typescript@5.4.5):
+  /@typescript-eslint/type-utils@5.62.0(eslint@8.46.0)(typescript@5.4.5):
     resolution: {integrity: sha512-xsSQreu+VnfbqQpW5vnCJdq1Z3Q0U31qiWmRhr98ONQmcp/yhiPJFPq8MXiJVLiksmOKSjIldZzkebzHuCGzew==}
     engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
     peerDependencies:
@@ -4744,16 +4737,16 @@ packages:
         optional: true
     dependencies:
       '@typescript-eslint/typescript-estree': 5.62.0(typescript@5.4.5)
-      '@typescript-eslint/utils': 5.62.0(eslint@9.0.0)(typescript@5.4.5)
+      '@typescript-eslint/utils': 5.62.0(eslint@8.46.0)(typescript@5.4.5)
       debug: 4.3.4(supports-color@8.1.1)
-      eslint: 9.0.0
+      eslint: 8.46.0
       tsutils: 3.21.0(typescript@5.4.5)
       typescript: 5.4.5
     transitivePeerDependencies:
       - supports-color
     dev: true
 
-  /@typescript-eslint/type-utils@6.21.0(eslint@9.0.0)(typescript@5.4.5):
+  /@typescript-eslint/type-utils@6.21.0(eslint@8.46.0)(typescript@5.4.5):
     resolution: {integrity: sha512-rZQI7wHfao8qMX3Rd3xqeYSMCL3SoiSQLBATSiVKARdFGCYSRvmViieZjqc58jKgs8Y8i9YvVVhRbHSTA4VBag==}
     engines: {node: ^16.0.0 || >=18.0.0}
     peerDependencies:
@@ -4764,9 +4757,9 @@ packages:
         optional: true
     dependencies:
       '@typescript-eslint/typescript-estree': 6.21.0(typescript@5.4.5)
-      '@typescript-eslint/utils': 6.21.0(eslint@9.0.0)(typescript@5.4.5)
+      '@typescript-eslint/utils': 6.21.0(eslint@8.46.0)(typescript@5.4.5)
       debug: 4.3.4(supports-color@8.1.1)
-      eslint: 9.0.0
+      eslint: 8.46.0
       ts-api-utils: 1.0.1(typescript@5.4.5)
       typescript: 5.4.5
     transitivePeerDependencies:
@@ -4867,19 +4860,19 @@ packages:
       - typescript
     dev: true
 
-  /@typescript-eslint/utils@5.62.0(eslint@9.0.0)(typescript@5.4.5):
+  /@typescript-eslint/utils@5.62.0(eslint@8.46.0)(typescript@5.4.5):
     resolution: {integrity: sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ==}
     engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
     peerDependencies:
       eslint: ^6.0.0 || ^7.0.0 || ^8.0.0
     dependencies:
-      '@eslint-community/eslint-utils': 4.4.0(eslint@9.0.0)
+      '@eslint-community/eslint-utils': 4.4.0(eslint@8.46.0)
       '@types/json-schema': 7.0.12
       '@types/semver': 7.5.8
       '@typescript-eslint/scope-manager': 5.62.0
       '@typescript-eslint/types': 5.62.0
       '@typescript-eslint/typescript-estree': 5.62.0(typescript@5.4.5)
-      eslint: 9.0.0
+      eslint: 8.46.0
       eslint-scope: 5.1.1
       semver: 7.6.0
     transitivePeerDependencies:
@@ -4887,19 +4880,19 @@ packages:
       - typescript
     dev: true
 
-  /@typescript-eslint/utils@6.21.0(eslint@9.0.0)(typescript@5.4.5):
+  /@typescript-eslint/utils@6.21.0(eslint@8.46.0)(typescript@5.4.5):
     resolution: {integrity: sha512-NfWVaC8HP9T8cbKQxHcsJBY5YE1O33+jpMwN45qzWWaPDZgLIbo12toGMWnmhvCpd3sIxkpDw3Wv1B3dYrbDQQ==}
     engines: {node: ^16.0.0 || >=18.0.0}
     peerDependencies:
       eslint: ^7.0.0 || ^8.0.0
     dependencies:
-      '@eslint-community/eslint-utils': 4.4.0(eslint@9.0.0)
+      '@eslint-community/eslint-utils': 4.4.0(eslint@8.46.0)
       '@types/json-schema': 7.0.14
       '@types/semver': 7.5.8
       '@typescript-eslint/scope-manager': 6.21.0
       '@typescript-eslint/types': 6.21.0
       '@typescript-eslint/typescript-estree': 6.21.0(typescript@5.4.5)
-      eslint: 9.0.0
+      eslint: 8.46.0
       semver: 7.6.0
     transitivePeerDependencies:
       - supports-color
@@ -8214,41 +8207,41 @@ packages:
     engines: {node: '>=12'}
     dev: true
 
-  /eslint-compat-utils@0.1.2(eslint@9.0.0):
+  /eslint-compat-utils@0.1.2(eslint@8.46.0):
     resolution: {integrity: sha512-Jia4JDldWnFNIru1Ehx1H5s9/yxiRHY/TimCuUc0jNexew3cF1gI6CYZil1ociakfWO3rRqFjl1mskBblB3RYg==}
     engines: {node: '>=12'}
     peerDependencies:
       eslint: '>=6.0.0'
     dependencies:
-      eslint: 9.0.0
+      eslint: 8.46.0
     dev: true
 
-  /eslint-compat-utils@0.4.1(eslint@9.0.0):
+  /eslint-compat-utils@0.4.1(eslint@8.46.0):
     resolution: {integrity: sha512-5N7ZaJG5pZxUeNNJfUchurLVrunD1xJvyg5kYOIVF8kg1f3ajTikmAu/5fZ9w100omNPOoMjngRszh/Q/uFGMg==}
     engines: {node: '>=12'}
     peerDependencies:
       eslint: '>=6.0.0'
     dependencies:
-      eslint: 9.0.0
+      eslint: 8.46.0
       semver: 7.6.0
     dev: true
 
-  /eslint-config-prettier@8.9.0(eslint@9.0.0):
+  /eslint-config-prettier@8.9.0(eslint@8.46.0):
     resolution: {integrity: sha512-+sbni7NfVXnOpnRadUA8S28AUlsZt9GjgFvABIRL9Hkn8KqNzOp+7Lw4QWtrwn20KzU3wqu1QoOj2m+7rKRqkA==}
     hasBin: true
     peerDependencies:
       eslint: '>=7.0.0'
     dependencies:
-      eslint: 9.0.0
+      eslint: 8.46.0
     dev: true
 
-  /eslint-config-prettier@9.1.0(eslint@9.0.0):
+  /eslint-config-prettier@9.1.0(eslint@8.46.0):
     resolution: {integrity: sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==}
     hasBin: true
     peerDependencies:
       eslint: '>=7.0.0'
     dependencies:
-      eslint: 9.0.0
+      eslint: 8.46.0
     dev: true
 
   /eslint-config-standard@16.0.3(eslint-plugin-import@2.28.0)(eslint-plugin-node@11.1.0)(eslint-plugin-promise@6.1.1)(eslint@8.46.0):
@@ -8318,7 +8311,7 @@ packages:
       - supports-color
     dev: true
 
-  /eslint-module-utils@2.8.0(@typescript-eslint/parser@5.62.0)(eslint-import-resolver-node@0.3.9)(eslint@9.0.0):
+  /eslint-module-utils@2.8.0(@typescript-eslint/parser@6.21.0)(eslint-import-resolver-node@0.3.9)(eslint@8.46.0):
     resolution: {integrity: sha512-aWajIYfsqCKRDgUfjEXNN/JlrzauMuSEy5sbd7WXbtW3EH6A6MpwEh42c7qD+MqQo9QMJ6fWLAeIJynx0g6OAw==}
     engines: {node: '>=4'}
     peerDependencies:
@@ -8339,53 +8332,24 @@ packages:
       eslint-import-resolver-webpack:
         optional: true
     dependencies:
-      '@typescript-eslint/parser': 5.62.0(eslint@9.0.0)(typescript@5.4.5)
+      '@typescript-eslint/parser': 6.21.0(eslint@8.46.0)(typescript@5.4.5)
       debug: 3.2.7
-      eslint: 9.0.0
+      eslint: 8.46.0
       eslint-import-resolver-node: 0.3.9
     transitivePeerDependencies:
       - supports-color
     dev: true
 
-  /eslint-module-utils@2.8.0(@typescript-eslint/parser@6.21.0)(eslint-import-resolver-node@0.3.9)(eslint@9.0.0):
-    resolution: {integrity: sha512-aWajIYfsqCKRDgUfjEXNN/JlrzauMuSEy5sbd7WXbtW3EH6A6MpwEh42c7qD+MqQo9QMJ6fWLAeIJynx0g6OAw==}
-    engines: {node: '>=4'}
-    peerDependencies:
-      '@typescript-eslint/parser': '*'
-      eslint: '*'
-      eslint-import-resolver-node: '*'
-      eslint-import-resolver-typescript: '*'
-      eslint-import-resolver-webpack: '*'
-    peerDependenciesMeta:
-      '@typescript-eslint/parser':
-        optional: true
-      eslint:
-        optional: true
-      eslint-import-resolver-node:
-        optional: true
-      eslint-import-resolver-typescript:
-        optional: true
-      eslint-import-resolver-webpack:
-        optional: true
-    dependencies:
-      '@typescript-eslint/parser': 6.21.0(eslint@9.0.0)(typescript@5.4.5)
-      debug: 3.2.7
-      eslint: 9.0.0
-      eslint-import-resolver-node: 0.3.9
-    transitivePeerDependencies:
-      - supports-color
-    dev: true
-
-  /eslint-plugin-es-x@7.5.0(eslint@9.0.0):
+  /eslint-plugin-es-x@7.5.0(eslint@8.46.0):
     resolution: {integrity: sha512-ODswlDSO0HJDzXU0XvgZ3lF3lS3XAZEossh15Q2UHjwrJggWeBoKqqEsLTZLXl+dh5eOAozG0zRcYtuE35oTuQ==}
     engines: {node: ^14.18.0 || >=16.0.0}
     peerDependencies:
       eslint: '>=8'
     dependencies:
-      '@eslint-community/eslint-utils': 4.4.0(eslint@9.0.0)
+      '@eslint-community/eslint-utils': 4.4.0(eslint@8.46.0)
       '@eslint-community/regexpp': 4.9.1
-      eslint: 9.0.0
-      eslint-compat-utils: 0.1.2(eslint@9.0.0)
+      eslint: 8.46.0
+      eslint-compat-utils: 0.1.2(eslint@8.46.0)
     dev: true
 
   /eslint-plugin-es@3.0.1(eslint@8.46.0):
@@ -8399,35 +8363,35 @@ packages:
       regexpp: 3.2.0
     dev: true
 
-  /eslint-plugin-es@4.1.0(eslint@9.0.0):
+  /eslint-plugin-es@4.1.0(eslint@8.46.0):
     resolution: {integrity: sha512-GILhQTnjYE2WorX5Jyi5i4dz5ALWxBIdQECVQavL6s7cI76IZTDWleTHkxz/QT3kvcs2QlGHvKLYsSlPOlPXnQ==}
     engines: {node: '>=8.10.0'}
     peerDependencies:
       eslint: '>=4.19.1'
     dependencies:
-      eslint: 9.0.0
+      eslint: 8.46.0
       eslint-utils: 2.1.0
       regexpp: 3.2.0
     dev: true
 
-  /eslint-plugin-eslint-comments@3.2.0(eslint@9.0.0):
+  /eslint-plugin-eslint-comments@3.2.0(eslint@8.46.0):
     resolution: {integrity: sha512-0jkOl0hfojIHHmEHgmNdqv4fmh7300NdpA9FFpF7zaoLvB/QeXOGNLIo86oAveJFrfB1p05kC8hpEMHM8DwWVQ==}
     engines: {node: '>=6.5.0'}
     peerDependencies:
       eslint: '>=4.19.1'
     dependencies:
       escape-string-regexp: 1.0.5
-      eslint: 9.0.0
+      eslint: 8.46.0
       ignore: 5.2.4
     dev: true
 
-  /eslint-plugin-file-progress@1.3.0(eslint@9.0.0):
+  /eslint-plugin-file-progress@1.3.0(eslint@8.46.0):
     resolution: {integrity: sha512-LncpnGHU26KPvCrvDC2Sl9PfjdrsG8qltgiK6BR7KybWtfqrdlsu1ax3+hyPMn5OkKBTF3Wki3oqK1MSMeOtQw==}
     peerDependencies:
       eslint: ^7.0.0 || ^8.0.0
     dependencies:
       chalk: 4.1.2
-      eslint: 9.0.0
+      eslint: 8.46.0
       ora: 5.4.1
     dev: true
 
@@ -8473,7 +8437,7 @@ packages:
       - supports-color
     dev: true
 
-  /eslint-plugin-import@2.29.0(@typescript-eslint/parser@5.62.0)(eslint@9.0.0):
+  /eslint-plugin-import@2.29.0(@typescript-eslint/parser@5.62.0)(eslint@8.46.0):
     resolution: {integrity: sha512-QPOO5NO6Odv5lpoTkddtutccQjysJuFxoPS7fAHO+9m9udNHvTCPSAMW9zGAYj8lAIdr40I8yPCdUYrncXtrwg==}
     engines: {node: '>=4'}
     peerDependencies:
@@ -8483,16 +8447,16 @@ packages:
       '@typescript-eslint/parser':
         optional: true
     dependencies:
-      '@typescript-eslint/parser': 5.62.0(eslint@9.0.0)(typescript@5.4.5)
+      '@typescript-eslint/parser': 5.62.0(eslint@8.46.0)(typescript@5.4.5)
       array-includes: 3.1.7
       array.prototype.findlastindex: 1.2.3
       array.prototype.flat: 1.3.2
       array.prototype.flatmap: 1.3.2
       debug: 3.2.7
       doctrine: 2.1.0
-      eslint: 9.0.0
+      eslint: 8.46.0
       eslint-import-resolver-node: 0.3.9
-      eslint-module-utils: 2.8.0(@typescript-eslint/parser@5.62.0)(eslint-import-resolver-node@0.3.9)(eslint@9.0.0)
+      eslint-module-utils: 2.8.0(@typescript-eslint/parser@5.62.0)(eslint-import-resolver-node@0.3.9)(eslint@8.46.0)
       hasown: 2.0.0
       is-core-module: 2.13.1
       is-glob: 4.0.3
@@ -8508,7 +8472,7 @@ packages:
       - supports-color
     dev: true
 
-  /eslint-plugin-import@2.29.1(@typescript-eslint/parser@6.21.0)(eslint@9.0.0):
+  /eslint-plugin-import@2.29.1(@typescript-eslint/parser@6.21.0)(eslint@8.46.0):
     resolution: {integrity: sha512-BbPC0cuExzhiMo4Ff1BTVwHpjjv28C5R+btTOGaCRC7UEz801up0JadwkeSk5Ued6TG34uaczuVuH6qyy5YUxw==}
     engines: {node: '>=4'}
     peerDependencies:
@@ -8518,16 +8482,16 @@ packages:
       '@typescript-eslint/parser':
         optional: true
     dependencies:
-      '@typescript-eslint/parser': 6.21.0(eslint@9.0.0)(typescript@5.4.5)
+      '@typescript-eslint/parser': 6.21.0(eslint@8.46.0)(typescript@5.4.5)
       array-includes: 3.1.7
       array.prototype.findlastindex: 1.2.3
       array.prototype.flat: 1.3.2
       array.prototype.flatmap: 1.3.2
       debug: 3.2.7
       doctrine: 2.1.0
-      eslint: 9.0.0
+      eslint: 8.46.0
       eslint-import-resolver-node: 0.3.9
-      eslint-module-utils: 2.8.0(@typescript-eslint/parser@6.21.0)(eslint-import-resolver-node@0.3.9)(eslint@9.0.0)
+      eslint-module-utils: 2.8.0(@typescript-eslint/parser@6.21.0)(eslint-import-resolver-node@0.3.9)(eslint@8.46.0)
       hasown: 2.0.0
       is-core-module: 2.13.1
       is-glob: 4.0.3
@@ -8543,7 +8507,7 @@ packages:
       - supports-color
     dev: true
 
-  /eslint-plugin-jsdoc@48.0.6(eslint@9.0.0):
+  /eslint-plugin-jsdoc@48.0.6(eslint@8.46.0):
     resolution: {integrity: sha512-LgwXOX6TWxxFYcbdVe+BJ94Kl/pgjSPYHLzqEdAMXTA1BH9WDx7iJ+9/iDajPF64LtzWX8C1mCfpbMZjJGhAOw==}
     engines: {node: '>=18'}
     peerDependencies:
@@ -8554,7 +8518,7 @@ packages:
       comment-parser: 1.4.1
       debug: 4.3.4(supports-color@8.1.1)
       escape-string-regexp: 4.0.0
-      eslint: 9.0.0
+      eslint: 8.46.0
       esquery: 1.5.0
       is-builtin-module: 3.2.1
       semver: 7.6.0
@@ -8563,28 +8527,28 @@ packages:
       - supports-color
     dev: true
 
-  /eslint-plugin-jsonc@2.10.0(eslint@9.0.0):
+  /eslint-plugin-jsonc@2.10.0(eslint@8.46.0):
     resolution: {integrity: sha512-9d//o6Jyh4s1RxC9fNSt1+MMaFN2ruFdXPG9XZcb/mR2KkfjADYiNL/hbU6W0Cyxfg3tS/XSFuhl5LgtMD8hmw==}
     engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
     peerDependencies:
       eslint: '>=6.0.0'
     dependencies:
-      '@eslint-community/eslint-utils': 4.4.0(eslint@9.0.0)
-      eslint: 9.0.0
-      eslint-compat-utils: 0.1.2(eslint@9.0.0)
+      '@eslint-community/eslint-utils': 4.4.0(eslint@8.46.0)
+      eslint: 8.46.0
+      eslint-compat-utils: 0.1.2(eslint@8.46.0)
       jsonc-eslint-parser: 2.3.0
       natural-compare: 1.4.0
     dev: true
 
-  /eslint-plugin-jsonc@2.13.0(eslint@9.0.0):
+  /eslint-plugin-jsonc@2.13.0(eslint@8.46.0):
     resolution: {integrity: sha512-2wWdJfpO/UbZzPDABuUVvlUQjfMJa2p2iQfYt/oWxOMpXCcjuiMUSaA02gtY/Dbu82vpaSqc+O7Xq6ECHwtIxA==}
     engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
     peerDependencies:
       eslint: '>=6.0.0'
     dependencies:
-      '@eslint-community/eslint-utils': 4.4.0(eslint@9.0.0)
-      eslint: 9.0.0
-      eslint-compat-utils: 0.4.1(eslint@9.0.0)
+      '@eslint-community/eslint-utils': 4.4.0(eslint@8.46.0)
+      eslint: 8.46.0
+      eslint-compat-utils: 0.4.1(eslint@8.46.0)
       espree: 9.6.1
       graphemer: 1.4.0
       jsonc-eslint-parser: 2.4.0
@@ -8592,28 +8556,28 @@ packages:
       synckit: 0.6.2
     dev: true
 
-  /eslint-plugin-markdown@3.0.1(eslint@9.0.0):
+  /eslint-plugin-markdown@3.0.1(eslint@8.46.0):
     resolution: {integrity: sha512-8rqoc148DWdGdmYF6WSQFT3uQ6PO7zXYgeBpHAOAakX/zpq+NvFYbDA/H7PYzHajwtmaOzAwfxyl++x0g1/N9A==}
     engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
     peerDependencies:
       eslint: ^6.0.0 || ^7.0.0 || ^8.0.0
     dependencies:
-      eslint: 9.0.0
+      eslint: 8.46.0
       mdast-util-from-markdown: 0.8.5
     transitivePeerDependencies:
       - supports-color
     dev: true
 
-  /eslint-plugin-n@15.7.0(eslint@9.0.0):
+  /eslint-plugin-n@15.7.0(eslint@8.46.0):
     resolution: {integrity: sha512-jDex9s7D/Qial8AGVIHq4W7NswpUD5DPDL2RH8Lzd9EloWUuvUkHfv4FRLMipH5q2UtyurorBkPeNi1wVWNh3Q==}
     engines: {node: '>=12.22.0'}
     peerDependencies:
       eslint: '>=7.0.0'
     dependencies:
       builtins: 5.0.1
-      eslint: 9.0.0
-      eslint-plugin-es: 4.1.0(eslint@9.0.0)
-      eslint-utils: 3.0.0(eslint@9.0.0)
+      eslint: 8.46.0
+      eslint-plugin-es: 4.1.0(eslint@8.46.0)
+      eslint-utils: 3.0.0(eslint@8.46.0)
       ignore: 5.2.4
       is-core-module: 2.13.1
       minimatch: 3.1.2
@@ -8621,16 +8585,16 @@ packages:
       semver: 7.6.0
     dev: true
 
-  /eslint-plugin-n@16.6.2(eslint@9.0.0):
+  /eslint-plugin-n@16.6.2(eslint@8.46.0):
     resolution: {integrity: sha512-6TyDmZ1HXoFQXnhCTUjVFULReoBPOAjpuiKELMkeP40yffI/1ZRO+d9ug/VC6fqISo2WkuIBk3cvuRPALaWlOQ==}
     engines: {node: '>=16.0.0'}
     peerDependencies:
       eslint: '>=7.0.0'
     dependencies:
-      '@eslint-community/eslint-utils': 4.4.0(eslint@9.0.0)
+      '@eslint-community/eslint-utils': 4.4.0(eslint@8.46.0)
       builtins: 5.0.1
-      eslint: 9.0.0
-      eslint-plugin-es-x: 7.5.0(eslint@9.0.0)
+      eslint: 8.46.0
+      eslint-plugin-es-x: 7.5.0(eslint@8.46.0)
       get-tsconfig: 4.7.2
       globals: 13.24.0
       ignore: 5.2.4
@@ -8656,7 +8620,7 @@ packages:
       semver: 6.3.1
     dev: true
 
-  /eslint-plugin-prettier@4.2.1(eslint-config-prettier@8.9.0)(eslint@9.0.0)(prettier@3.2.5):
+  /eslint-plugin-prettier@4.2.1(eslint-config-prettier@8.9.0)(eslint@8.46.0)(prettier@3.2.5):
     resolution: {integrity: sha512-f/0rXLXUt0oFYs8ra4w49wYZBG5GKZpAYsJSm6rnYL5uVDjd+zowwMwVZHnAjf4edNrKpCDYfXDgmRE/Ak7QyQ==}
     engines: {node: '>=12.0.0'}
     peerDependencies:
@@ -8667,13 +8631,13 @@ packages:
       eslint-config-prettier:
         optional: true
     dependencies:
-      eslint: 9.0.0
-      eslint-config-prettier: 8.9.0(eslint@9.0.0)
+      eslint: 8.46.0
+      eslint-config-prettier: 8.9.0(eslint@8.46.0)
       prettier: 3.2.5
       prettier-linter-helpers: 1.0.0
     dev: true
 
-  /eslint-plugin-prettier@5.1.3(eslint-config-prettier@9.1.0)(eslint@9.0.0)(prettier@3.2.5):
+  /eslint-plugin-prettier@5.1.3(eslint-config-prettier@9.1.0)(eslint@8.46.0)(prettier@3.2.5):
     resolution: {integrity: sha512-C9GCVAs4Eq7ZC/XFQHITLiHJxQngdtraXaM+LoUFoFp/lHNl2Zn8f3WQbe9HvTBBQ9YnKFB0/2Ajdqwo5D1EAw==}
     engines: {node: ^14.18.0 || >=16.0.0}
     peerDependencies:
@@ -8687,20 +8651,20 @@ packages:
       eslint-config-prettier:
         optional: true
     dependencies:
-      eslint: 9.0.0
-      eslint-config-prettier: 9.1.0(eslint@9.0.0)
+      eslint: 8.46.0
+      eslint-config-prettier: 9.1.0(eslint@8.46.0)
       prettier: 3.2.5
       prettier-linter-helpers: 1.0.0
       synckit: 0.8.8
     dev: true
 
-  /eslint-plugin-promise@5.2.0(eslint@9.0.0):
+  /eslint-plugin-promise@5.2.0(eslint@8.46.0):
     resolution: {integrity: sha512-SftLb1pUG01QYq2A/hGAWfDRXqYD82zE7j7TopDOyNdU+7SvvoXREls/+PRTY17vUXzXnZA/zfnyKgRH6x4JJw==}
     engines: {node: ^10.12.0 || >=12.0.0}
     peerDependencies:
       eslint: ^7.0.0
     dependencies:
-      eslint: 9.0.0
+      eslint: 8.46.0
     dev: true
 
   /eslint-plugin-promise@6.1.1(eslint@8.46.0):
@@ -8712,15 +8676,6 @@ packages:
       eslint: 8.46.0
     dev: true
 
-  /eslint-plugin-promise@6.1.1(eslint@9.0.0):
-    resolution: {integrity: sha512-tjqWDwVZQo7UIPMeDReOpUgHCmCiH+ePnVT+5zVapL0uuHnegBUs2smM13CzOs2Xb5+MHMRFTs9v24yjba4Oig==}
-    engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
-    peerDependencies:
-      eslint: ^7.0.0 || ^8.0.0
-    dependencies:
-      eslint: 9.0.0
-    dev: true
-
   /eslint-plugin-standard@5.0.0(eslint@8.46.0):
     resolution: {integrity: sha512-eSIXPc9wBM4BrniMzJRBm2uoVuXz2EPa+NXPk2+itrVt+r5SbKFERx/IgrK/HmfjddyKVz2f+j+7gBRvu19xLg==}
     deprecated: 'standard 16.0.0 and eslint-config-standard 16.0.0 no longer require the eslint-plugin-standard package. You can remove it from your dependencies with ''npm rm eslint-plugin-standard''. More info here: https://github.com/standard/standard/issues/1316'
@@ -8737,7 +8692,7 @@ packages:
       '@microsoft/tsdoc-config': 0.16.2
     dev: true
 
-  /eslint-plugin-unicorn@40.1.0(eslint@9.0.0):
+  /eslint-plugin-unicorn@40.1.0(eslint@8.46.0):
     resolution: {integrity: sha512-y5doK2DF9Sr5AqKEHbHxjFllJ167nKDRU01HDcWyv4Tnmaoe9iNxMrBnaybZvWZUaE3OC5Unu0lNIevYamloig==}
     engines: {node: '>=12'}
     peerDependencies:
@@ -8746,8 +8701,8 @@ packages:
       '@babel/helper-validator-identifier': 7.22.20
       ci-info: 3.9.0
       clean-regexp: 1.0.0
-      eslint: 9.0.0
-      eslint-utils: 3.0.0(eslint@9.0.0)
+      eslint: 8.46.0
+      eslint-utils: 3.0.0(eslint@8.46.0)
       esquery: 1.5.0
       indent-string: 4.0.0
       is-builtin-module: 3.2.1
@@ -8760,17 +8715,17 @@ packages:
       strip-indent: 3.0.0
     dev: true
 
-  /eslint-plugin-unicorn@45.0.2(eslint@9.0.0):
+  /eslint-plugin-unicorn@45.0.2(eslint@8.46.0):
     resolution: {integrity: sha512-Y0WUDXRyGDMcKLiwgL3zSMpHrXI00xmdyixEGIg90gHnj0PcHY4moNv3Ppje/kDivdAy5vUeUr7z211ImPv2gw==}
     engines: {node: '>=14.18'}
     peerDependencies:
       eslint: '>=8.28.0'
     dependencies:
       '@babel/helper-validator-identifier': 7.22.5
-      '@eslint-community/eslint-utils': 4.4.0(eslint@9.0.0)
+      '@eslint-community/eslint-utils': 4.4.0(eslint@8.46.0)
       ci-info: 3.9.0
       clean-regexp: 1.0.0
-      eslint: 9.0.0
+      eslint: 8.46.0
       esquery: 1.5.0
       indent-string: 4.0.0
       is-builtin-module: 3.2.1
@@ -8789,92 +8744,92 @@ packages:
     resolution: {integrity: sha512-WE+YlK9X9s4vf5EaYRU0Scw7WItDZStm+PapFSYlg2ABNtaQ4zIG7wEqpoUB3SlfM+SgkhgmzR0TeJOO5k3/Nw==}
     dev: true
 
-  /eslint-plugin-vue-scoped-css@2.5.0(eslint@9.0.0)(vue-eslint-parser@9.3.1):
+  /eslint-plugin-vue-scoped-css@2.5.0(eslint@8.46.0)(vue-eslint-parser@9.3.1):
     resolution: {integrity: sha512-vR+raYNE1aQ69lS1lZGiKoz8rXFI3MWf2fxrfns/XCQ0XT5sIguhDtQS+9JmUQJClenLDEe2CQx7P+eeSdF4cA==}
     engines: {node: ^12.22 || ^14.17 || >=16}
     peerDependencies:
       eslint: '>=5.0.0'
       vue-eslint-parser: '>=7.1.0'
     dependencies:
-      eslint: 9.0.0
-      eslint-utils: 3.0.0(eslint@9.0.0)
+      eslint: 8.46.0
+      eslint-utils: 3.0.0(eslint@8.46.0)
       lodash: 4.17.21
       postcss: 8.4.31
       postcss-safe-parser: 6.0.0(postcss@8.4.31)
       postcss-scss: 4.0.6(postcss@8.4.31)
       postcss-selector-parser: 6.0.13
       postcss-styl: 0.12.3
-      vue-eslint-parser: 9.3.1(eslint@9.0.0)
+      vue-eslint-parser: 9.3.1(eslint@8.46.0)
     transitivePeerDependencies:
       - supports-color
     dev: true
 
-  /eslint-plugin-vue-scoped-css@2.7.2(eslint@9.0.0)(vue-eslint-parser@9.4.2):
+  /eslint-plugin-vue-scoped-css@2.7.2(eslint@8.46.0)(vue-eslint-parser@9.4.2):
     resolution: {integrity: sha512-myJ99CJuwmAx5kq1WjgIeaUkxeU6PIEUh7age79Alm30bhN4fVTapOQLSMlvVTgxr36Y3igsZ3BCJM32LbHHig==}
     engines: {node: ^12.22 || ^14.17 || >=16}
     peerDependencies:
       eslint: '>=5.0.0'
       vue-eslint-parser: '>=7.1.0'
     dependencies:
-      '@eslint-community/eslint-utils': 4.4.0(eslint@9.0.0)
-      eslint: 9.0.0
-      eslint-compat-utils: 0.4.1(eslint@9.0.0)
+      '@eslint-community/eslint-utils': 4.4.0(eslint@8.46.0)
+      eslint: 8.46.0
+      eslint-compat-utils: 0.4.1(eslint@8.46.0)
       lodash: 4.17.21
       postcss: 8.4.31
       postcss-safe-parser: 6.0.0(postcss@8.4.31)
       postcss-scss: 4.0.6(postcss@8.4.31)
       postcss-selector-parser: 6.0.13
       postcss-styl: 0.12.3
-      vue-eslint-parser: 9.4.2(eslint@9.0.0)
+      vue-eslint-parser: 9.4.2(eslint@8.46.0)
     transitivePeerDependencies:
       - supports-color
     dev: true
 
-  /eslint-plugin-vue@9.16.1(eslint@9.0.0):
+  /eslint-plugin-vue@9.16.1(eslint@8.46.0):
     resolution: {integrity: sha512-2FtnTqazA6aYONfDuOZTk0QzwhAwi7Z4+uJ7+GHeGxcKapjqWlDsRWDenvyG/utyOfAS5bVRmAG3cEWiYEz2bA==}
     engines: {node: ^14.17.0 || >=16.0.0}
     peerDependencies:
       eslint: ^6.2.0 || ^7.0.0 || ^8.0.0
     dependencies:
-      '@eslint-community/eslint-utils': 4.4.0(eslint@9.0.0)
-      eslint: 9.0.0
+      '@eslint-community/eslint-utils': 4.4.0(eslint@8.46.0)
+      eslint: 8.46.0
       natural-compare: 1.4.0
       nth-check: 2.1.1
       postcss-selector-parser: 6.0.13
       semver: 7.6.0
-      vue-eslint-parser: 9.3.2(eslint@9.0.0)
+      vue-eslint-parser: 9.3.2(eslint@8.46.0)
       xml-name-validator: 4.0.0
     transitivePeerDependencies:
       - supports-color
     dev: true
 
-  /eslint-plugin-vue@9.21.1(eslint@9.0.0):
+  /eslint-plugin-vue@9.21.1(eslint@8.46.0):
     resolution: {integrity: sha512-XVtI7z39yOVBFJyi8Ljbn7kY9yHzznKXL02qQYn+ta63Iy4A9JFBw6o4OSB9hyD2++tVT+su9kQqetUyCCwhjw==}
     engines: {node: ^14.17.0 || >=16.0.0}
     peerDependencies:
       eslint: ^6.2.0 || ^7.0.0 || ^8.0.0
     dependencies:
-      '@eslint-community/eslint-utils': 4.4.0(eslint@9.0.0)
-      eslint: 9.0.0
+      '@eslint-community/eslint-utils': 4.4.0(eslint@8.46.0)
+      eslint: 8.46.0
       natural-compare: 1.4.0
       nth-check: 2.1.1
       postcss-selector-parser: 6.0.13
       semver: 7.6.0
-      vue-eslint-parser: 9.4.2(eslint@9.0.0)
+      vue-eslint-parser: 9.4.2(eslint@8.46.0)
       xml-name-validator: 4.0.0
     transitivePeerDependencies:
       - supports-color
     dev: true
 
-  /eslint-plugin-yml@1.10.0(eslint@9.0.0):
+  /eslint-plugin-yml@1.10.0(eslint@8.46.0):
     resolution: {integrity: sha512-53SUwuNDna97lVk38hL/5++WXDuugPM9SUQ1T645R0EHMRCdBIIxGye/oOX2qO3FQ7aImxaUZJU/ju+NMUBrLQ==}
     engines: {node: ^14.17.0 || >=16.0.0}
     peerDependencies:
       eslint: '>=6.0.0'
     dependencies:
       debug: 4.3.4(supports-color@8.1.1)
-      eslint: 9.0.0
-      eslint-compat-utils: 0.1.2(eslint@9.0.0)
+      eslint: 8.46.0
+      eslint-compat-utils: 0.1.2(eslint@8.46.0)
       lodash: 4.17.21
       natural-compare: 1.4.0
       yaml-eslint-parser: 1.2.2
@@ -8882,15 +8837,15 @@ packages:
       - supports-color
     dev: true
 
-  /eslint-plugin-yml@1.12.2(eslint@9.0.0):
+  /eslint-plugin-yml@1.12.2(eslint@8.46.0):
     resolution: {integrity: sha512-hvS9p08FhPT7i/ynwl7/Wt7ke7Rf4P2D6fT8lZlL43peZDTsHtH2A0SIFQ7Kt7+mJ6if6P+FX3iJhMkdnxQwpg==}
     engines: {node: ^14.17.0 || >=16.0.0}
     peerDependencies:
       eslint: '>=6.0.0'
     dependencies:
       debug: 4.3.4(supports-color@8.1.1)
-      eslint: 9.0.0
-      eslint-compat-utils: 0.4.1(eslint@9.0.0)
+      eslint: 8.46.0
+      eslint-compat-utils: 0.4.1(eslint@8.46.0)
       lodash: 4.17.21
       natural-compare: 1.4.0
       yaml-eslint-parser: 1.2.2
@@ -8933,13 +8888,13 @@ packages:
       eslint-visitor-keys: 1.3.0
     dev: true
 
-  /eslint-utils@3.0.0(eslint@9.0.0):
+  /eslint-utils@3.0.0(eslint@8.46.0):
     resolution: {integrity: sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==}
     engines: {node: ^10.0.0 || ^12.0.0 || >= 14.0.0}
     peerDependencies:
       eslint: '>=5'
     dependencies:
-      eslint: 9.0.0
+      eslint: 8.46.0
       eslint-visitor-keys: 2.1.0
     dev: true
 
@@ -17242,14 +17197,14 @@ packages:
       '@types/sortablejs': 1.15.8
     dev: true
 
-  /vue-eslint-parser@9.3.1(eslint@9.0.0):
+  /vue-eslint-parser@9.3.1(eslint@8.46.0):
     resolution: {integrity: sha512-Clr85iD2XFZ3lJ52/ppmUDG/spxQu6+MAeHXjjyI4I1NUYZ9xmenQp4N0oaHJhrA8OOxltCVxMRfANGa70vU0g==}
     engines: {node: ^14.17.0 || >=16.0.0}
     peerDependencies:
       eslint: '>=6.0.0'
     dependencies:
       debug: 4.3.4(supports-color@8.1.1)
-      eslint: 9.0.0
+      eslint: 8.46.0
       eslint-scope: 7.2.2
       eslint-visitor-keys: 3.4.3
       espree: 9.6.1
@@ -17260,14 +17215,14 @@ packages:
       - supports-color
     dev: true
 
-  /vue-eslint-parser@9.3.2(eslint@9.0.0):
+  /vue-eslint-parser@9.3.2(eslint@8.46.0):
     resolution: {integrity: sha512-q7tWyCVaV9f8iQyIA5Mkj/S6AoJ9KBN8IeUSf3XEmBrOtxOZnfTg5s4KClbZBCK3GtnT/+RyCLZyDHuZwTuBjg==}
     engines: {node: ^14.17.0 || >=16.0.0}
     peerDependencies:
       eslint: '>=6.0.0'
     dependencies:
       debug: 4.3.4(supports-color@8.1.1)
-      eslint: 9.0.0
+      eslint: 8.46.0
       eslint-scope: 7.2.2
       eslint-visitor-keys: 3.4.3
       espree: 9.6.1
@@ -17278,14 +17233,14 @@ packages:
       - supports-color
     dev: true
 
-  /vue-eslint-parser@9.4.2(eslint@9.0.0):
+  /vue-eslint-parser@9.4.2(eslint@8.46.0):
     resolution: {integrity: sha512-Ry9oiGmCAK91HrKMtCrKFWmSFWvYkpGglCeFAIqDdr9zdXmMMpJOmUJS7WWsW7fX81h6mwHmUZCQQ1E0PkSwYQ==}
     engines: {node: ^14.17.0 || >=16.0.0}
     peerDependencies:
       eslint: '>=6.0.0'
     dependencies:
       debug: 4.3.4(supports-color@8.1.1)
-      eslint: 9.0.0
+      eslint: 8.46.0
       eslint-scope: 7.2.2
       eslint-visitor-keys: 3.4.3
       espree: 9.6.1

From 0f3126196f49b2eb57b08bc21929b3c115476d3a Mon Sep 17 00:00:00 2001
From: naskya <m@naskya.net>
Date: Mon, 15 Apr 2024 10:02:44 +0900
Subject: [PATCH 065/110] refactor (backend): port reaction-lib to backend-rs

---
 packages/backend-rs/index.d.ts                |   8 +
 packages/backend-rs/index.js                  |   5 +-
 packages/backend-rs/src/misc/mod.rs           |   1 +
 packages/backend-rs/src/misc/reaction.rs      | 191 ++++++++++++++++++
 packages/backend/src/misc/populate-emojis.ts  |   3 +-
 packages/backend/src/misc/reaction-lib.ts     |  83 --------
 .../src/models/repositories/note-reaction.ts  |   2 +-
 .../backend/src/models/repositories/note.ts   |   5 +-
 .../src/services/note/reaction/create.ts      |   5 +-
 .../src/services/note/reaction/delete.ts      |   2 +-
 10 files changed, 211 insertions(+), 94 deletions(-)
 create mode 100644 packages/backend-rs/src/misc/reaction.rs
 delete mode 100644 packages/backend/src/misc/reaction-lib.ts

diff --git a/packages/backend-rs/index.d.ts b/packages/backend-rs/index.d.ts
index 9b8a64142f..a47495508e 100644
--- a/packages/backend-rs/index.d.ts
+++ b/packages/backend-rs/index.d.ts
@@ -156,6 +156,14 @@ export function nyaify(text: string, lang?: string | undefined | null): string
 export function hashPassword(password: string): string
 export function verifyPassword(password: string, hash: string): boolean
 export function isOldPasswordAlgorithm(hash: string): boolean
+export interface DecodedReaction {
+  reaction: string
+  name: string | null
+  host: string | null
+}
+export function decodeReaction(reaction: string): DecodedReaction
+export function countReactions(reactions: Record<string, number>): Record<string, number>
+export function toDbReaction(reaction?: string | undefined | null, host?: string | undefined | null): Promise<string>
 export interface AbuseUserReport {
   id: string
   createdAt: Date
diff --git a/packages/backend-rs/index.js b/packages/backend-rs/index.js
index 16de16297f..6fca851430 100644
--- a/packages/backend-rs/index.js
+++ b/packages/backend-rs/index.js
@@ -310,7 +310,7 @@ if (!nativeBinding) {
   throw new Error(`Failed to load native binding`)
 }
 
-const { readServerConfig, stringToAcct, acctToString, checkWordMute, getFullApAccount, isSelfHost, isSameOrigin, extractHost, toPuny, isUnicodeEmoji, sqlLikeEscape, safeForSql, formatMilliseconds, toMastodonId, fromMastodonId, fetchMeta, metaToPugArgs, nyaify, hashPassword, verifyPassword, isOldPasswordAlgorithm, AntennaSrcEnum, MutedNoteReasonEnum, NoteVisibilityEnum, NotificationTypeEnum, PageVisibilityEnum, PollNotevisibilityEnum, RelayStatusEnum, UserEmojimodpermEnum, UserProfileFfvisibilityEnum, UserProfileMutingnotificationtypesEnum, initIdGenerator, getTimestamp, genId, secureRndstr } = nativeBinding
+const { readServerConfig, stringToAcct, acctToString, checkWordMute, getFullApAccount, isSelfHost, isSameOrigin, extractHost, toPuny, isUnicodeEmoji, sqlLikeEscape, safeForSql, formatMilliseconds, toMastodonId, fromMastodonId, fetchMeta, metaToPugArgs, nyaify, hashPassword, verifyPassword, isOldPasswordAlgorithm, decodeReaction, countReactions, toDbReaction, AntennaSrcEnum, MutedNoteReasonEnum, NoteVisibilityEnum, NotificationTypeEnum, PageVisibilityEnum, PollNotevisibilityEnum, RelayStatusEnum, UserEmojimodpermEnum, UserProfileFfvisibilityEnum, UserProfileMutingnotificationtypesEnum, initIdGenerator, getTimestamp, genId, secureRndstr } = nativeBinding
 
 module.exports.readServerConfig = readServerConfig
 module.exports.stringToAcct = stringToAcct
@@ -333,6 +333,9 @@ module.exports.nyaify = nyaify
 module.exports.hashPassword = hashPassword
 module.exports.verifyPassword = verifyPassword
 module.exports.isOldPasswordAlgorithm = isOldPasswordAlgorithm
+module.exports.decodeReaction = decodeReaction
+module.exports.countReactions = countReactions
+module.exports.toDbReaction = toDbReaction
 module.exports.AntennaSrcEnum = AntennaSrcEnum
 module.exports.MutedNoteReasonEnum = MutedNoteReasonEnum
 module.exports.NoteVisibilityEnum = NoteVisibilityEnum
diff --git a/packages/backend-rs/src/misc/mod.rs b/packages/backend-rs/src/misc/mod.rs
index 3fd447f4d5..45fd31cdcd 100644
--- a/packages/backend-rs/src/misc/mod.rs
+++ b/packages/backend-rs/src/misc/mod.rs
@@ -8,3 +8,4 @@ pub mod mastodon_id;
 pub mod meta;
 pub mod nyaify;
 pub mod password;
+pub mod reaction;
diff --git a/packages/backend-rs/src/misc/reaction.rs b/packages/backend-rs/src/misc/reaction.rs
new file mode 100644
index 0000000000..a29ddf95de
--- /dev/null
+++ b/packages/backend-rs/src/misc/reaction.rs
@@ -0,0 +1,191 @@
+use crate::database::db_conn;
+use crate::misc::{convert_host::to_puny, emoji::is_unicode_emoji, meta::fetch_meta};
+use crate::model::entity::emoji;
+use once_cell::sync::Lazy;
+use regex::Regex;
+use sea_orm::prelude::*;
+use std::collections::HashMap;
+
+#[derive(PartialEq, Debug)]
+#[crate::export(object)]
+pub struct DecodedReaction {
+    pub reaction: String,
+    pub name: Option<String>,
+    pub host: Option<String>,
+}
+
+#[crate::export]
+pub fn decode_reaction(reaction: &str) -> DecodedReaction {
+    // Misskey allows you to include "+" and "-" in emoji shortcodes
+    // MFM spec: https://github.com/misskey-dev/mfm.js/blob/6aaf68089023c6adebe44123eebbc4dcd75955e0/docs/syntax.md?plain=1#L583
+    // Misskey's implementation: https://github.com/misskey-dev/misskey/blob/bba3097765317cbf95d09627961b5b5dce16a972/packages/backend/src/core/ReactionService.ts#L68
+    static RE: Lazy<Regex> =
+        Lazy::new(|| Regex::new(r"^:([0-9A-Za-z_+-]+)(?:@([0-9A-Za-z_.-]+))?:$").unwrap());
+
+    if let Some(captures) = RE.captures(reaction) {
+        let name = &captures[1];
+        let host = captures.get(2).map(|s| s.as_str());
+
+        DecodedReaction {
+            reaction: format!(":{}@{}:", name, host.unwrap_or(".")),
+            name: Some(name.to_owned()),
+            host: host.map(|s| s.to_owned()),
+        }
+    } else {
+        DecodedReaction {
+            reaction: reaction.to_owned(),
+            name: None,
+            host: None,
+        }
+    }
+}
+
+#[crate::export]
+pub fn count_reactions(reactions: &HashMap<String, u32>) -> HashMap<String, u32> {
+    let mut res = HashMap::<String, u32>::new();
+
+    for (reaction, count) in reactions.iter() {
+        if count > &0 {
+            let decoded = decode_reaction(reaction).reaction;
+            let total = res.entry(decoded).or_insert(0);
+            *total += count;
+        }
+    }
+
+    res
+}
+
+#[derive(thiserror::Error, Debug)]
+pub enum Error {
+    #[error("Idna error: {0}")]
+    IdnaError(#[from] idna::Errors),
+    #[error("Database error: {0}")]
+    DbError(#[from] DbErr),
+}
+
+#[crate::export]
+pub async fn to_db_reaction(reaction: Option<&str>, host: Option<&str>) -> Result<String, Error> {
+    if let Some(reaction) = reaction {
+        // FIXME: Is it okay to do this only here?
+        // This was introduced in https://firefish.dev/firefish/firefish/-/commit/af730e75b6fc1a57ca680ce83459d7e433b130cf
+        if reaction.contains('❤') || reaction.contains("♥️") {
+            return Ok("❤️".to_owned());
+        }
+
+        if is_unicode_emoji(reaction) {
+            return Ok(reaction.to_owned());
+        }
+
+        static RE: Lazy<Regex> =
+            Lazy::new(|| Regex::new(r"^:([0-9A-Za-z_+-]+)(?:@\.)?:$").unwrap());
+
+        if let Some(captures) = RE.captures(reaction) {
+            let name = &captures[1];
+            let db = db_conn().await?;
+
+            if let Some(host) = host {
+                // remote emoji
+                let ascii_host = to_puny(host)?;
+
+                // TODO: Does SeaORM have the `exists` method?
+                if emoji::Entity::find()
+                    .filter(emoji::Column::Name.eq(name))
+                    .filter(emoji::Column::Host.eq(&ascii_host))
+                    .one(db)
+                    .await?
+                    .is_some()
+                {
+                    return Ok(format!(":{name}@{ascii_host}:"));
+                }
+            } else {
+                // local emoji
+                // TODO: Does SeaORM have the `exists` method?
+                if emoji::Entity::find()
+                    .filter(emoji::Column::Name.eq(name))
+                    .filter(emoji::Column::Host.is_null())
+                    .one(db)
+                    .await?
+                    .is_some()
+                {
+                    return Ok(format!(":{name}:"));
+                }
+            }
+        };
+    };
+
+    Ok(fetch_meta(true).await?.default_reaction)
+}
+
+#[cfg(test)]
+mod unit_test {
+    use super::{decode_reaction, DecodedReaction};
+    use pretty_assertions::{assert_eq, assert_ne};
+
+    #[test]
+    fn test_decode_reaction() {
+        let unicode_emoji_1 = DecodedReaction {
+            reaction: "⭐".to_string(),
+            name: None,
+            host: None,
+        };
+        let unicode_emoji_2 = DecodedReaction {
+            reaction: "🩷".to_string(),
+            name: None,
+            host: None,
+        };
+
+        assert_eq!(decode_reaction("⭐"), unicode_emoji_1);
+        assert_eq!(decode_reaction("🩷"), unicode_emoji_2);
+
+        assert_ne!(decode_reaction("⭐"), unicode_emoji_2);
+        assert_ne!(decode_reaction("🩷"), unicode_emoji_1);
+
+        let unicode_emoji_3 = DecodedReaction {
+            reaction: "🖖🏿".to_string(),
+            name: None,
+            host: None,
+        };
+        assert_eq!(decode_reaction("🖖🏿"), unicode_emoji_3);
+
+        let local_emoji = DecodedReaction {
+            reaction: ":meow_melt_tears@.:".to_string(),
+            name: Some("meow_melt_tears".to_string()),
+            host: None,
+        };
+        assert_eq!(decode_reaction(":meow_melt_tears:"), local_emoji);
+
+        let remote_emoji_1 = DecodedReaction {
+            reaction: ":meow_uwu@some-domain.example.org:".to_string(),
+            name: Some("meow_uwu".to_string()),
+            host: Some("some-domain.example.org".to_string()),
+        };
+        assert_eq!(
+            decode_reaction(":meow_uwu@some-domain.example.org:"),
+            remote_emoji_1
+        );
+
+        let remote_emoji_2 = DecodedReaction {
+            reaction: ":C++23@xn--eckwd4c7c.example.org:".to_string(),
+            name: Some("C++23".to_string()),
+            host: Some("xn--eckwd4c7c.example.org".to_string()),
+        };
+        assert_eq!(
+            decode_reaction(":C++23@xn--eckwd4c7c.example.org:"),
+            remote_emoji_2
+        );
+
+        let invalid_reaction_1 = DecodedReaction {
+            reaction: ":foo".to_string(),
+            name: None,
+            host: None,
+        };
+        assert_eq!(decode_reaction(":foo"), invalid_reaction_1);
+
+        let invalid_reaction_2 = DecodedReaction {
+            reaction: ":foo&@example.com:".to_string(),
+            name: None,
+            host: None,
+        };
+        assert_eq!(decode_reaction(":foo&@example.com:"), invalid_reaction_2);
+    }
+}
diff --git a/packages/backend/src/misc/populate-emojis.ts b/packages/backend/src/misc/populate-emojis.ts
index f18b23c9a4..4ca60b222f 100644
--- a/packages/backend/src/misc/populate-emojis.ts
+++ b/packages/backend/src/misc/populate-emojis.ts
@@ -3,8 +3,7 @@ import { Emojis } from "@/models/index.js";
 import type { Emoji } from "@/models/entities/emoji.js";
 import type { Note } from "@/models/entities/note.js";
 import { Cache } from "./cache.js";
-import { isSelfHost, toPuny } from "backend-rs";
-import { decodeReaction } from "./reaction-lib.js";
+import { decodeReaction, isSelfHost, toPuny } from "backend-rs";
 import config from "@/config/index.js";
 import { query } from "@/prelude/url.js";
 import { redisClient } from "@/db/redis.js";
diff --git a/packages/backend/src/misc/reaction-lib.ts b/packages/backend/src/misc/reaction-lib.ts
deleted file mode 100644
index ac9ba5652d..0000000000
--- a/packages/backend/src/misc/reaction-lib.ts
+++ /dev/null
@@ -1,83 +0,0 @@
-import { fetchMeta, isUnicodeEmoji, toPuny } from "backend-rs";
-import { Emojis } from "@/models/index.js";
-import { IsNull } from "typeorm";
-
-export function convertReactions(reactions: Record<string, number>) {
-	const result = new Map();
-
-	for (const reaction in reactions) {
-		if (reactions[reaction] <= 0) continue;
-
-		const decoded = decodeReaction(reaction).reaction;
-		result.set(decoded, (result.get(decoded) || 0) + reactions[reaction]);
-	}
-
-	return Object.fromEntries(result);
-}
-
-export async function toDbReaction(
-	reaction?: string | null,
-	reacterHost?: string | null,
-): Promise<string> {
-	if (!reaction) return (await fetchMeta(true)).defaultReaction;
-
-	if (reaction.includes("❤") || reaction.includes("♥️")) return "❤️";
-
-	// Allow unicode reactions
-	if (isUnicodeEmoji(reaction)) {
-		return reaction;
-	}
-
-	reacterHost = reacterHost == null ? null : toPuny(reacterHost);
-
-	const custom = reaction.match(/^:([\w+-]+)(?:@\.)?:$/);
-	if (custom) {
-		const name = custom[1];
-		const emoji = await Emojis.findOneBy({
-			host: reacterHost || IsNull(),
-			name,
-		});
-
-		if (emoji) return reacterHost ? `:${name}@${reacterHost}:` : `:${name}:`;
-	}
-
-	return (await fetchMeta(true)).defaultReaction;
-}
-
-type DecodedReaction = {
-	/**
-	 * リアクション名 (Unicode Emoji or ':name@hostname' or ':name@.')
-	 */
-	reaction: string;
-
-	/**
-	 * name (カスタム絵文字の場合name, Emojiクエリに使う)
-	 */
-	name?: string;
-
-	/**
-	 * host (カスタム絵文字の場合host, Emojiクエリに使う)
-	 */
-	host?: string | null;
-};
-
-export function decodeReaction(str: string): DecodedReaction {
-	const custom = str.match(/^:([\w+-]+)(?:@([\w.-]+))?:$/);
-
-	if (custom) {
-		const name = custom[1];
-		const host = custom[2] || null;
-
-		return {
-			reaction: `:${name}@${host || "."}:`, // ローカル分は@以降を省略するのではなく.にする
-			name,
-			host,
-		};
-	}
-
-	return {
-		reaction: str,
-		name: undefined,
-		host: undefined,
-	};
-}
diff --git a/packages/backend/src/models/repositories/note-reaction.ts b/packages/backend/src/models/repositories/note-reaction.ts
index 20aae2876f..47e16ced0c 100644
--- a/packages/backend/src/models/repositories/note-reaction.ts
+++ b/packages/backend/src/models/repositories/note-reaction.ts
@@ -2,7 +2,7 @@ import { db } from "@/db/postgre.js";
 import { NoteReaction } from "@/models/entities/note-reaction.js";
 import { Notes, Users } from "../index.js";
 import type { Packed } from "@/misc/schema.js";
-import { decodeReaction } from "@/misc/reaction-lib.js";
+import { decodeReaction } from "backend-rs";
 import type { User } from "@/models/entities/user.js";
 
 export const NoteReactionRepository = db.getRepository(NoteReaction).extend({
diff --git a/packages/backend/src/models/repositories/note.ts b/packages/backend/src/models/repositories/note.ts
index ed6ecc4a5c..1bf4ef657f 100644
--- a/packages/backend/src/models/repositories/note.ts
+++ b/packages/backend/src/models/repositories/note.ts
@@ -12,9 +12,8 @@ import {
 	Channels,
 } from "../index.js";
 import type { Packed } from "@/misc/schema.js";
-import { nyaify } from "backend-rs";
+import { countReactions, decodeReaction, nyaify } from "backend-rs";
 import { awaitAll } from "@/prelude/await-all.js";
-import { convertReactions, decodeReaction } from "@/misc/reaction-lib.js";
 import type { NoteReaction } from "@/models/entities/note-reaction.js";
 import {
 	aggregateNoteEmojis,
@@ -214,7 +213,7 @@ export const NoteRepository = db.getRepository(Note).extend({
 				note.visibility === "specified" ? note.visibleUserIds : undefined,
 			renoteCount: note.renoteCount,
 			repliesCount: note.repliesCount,
-			reactions: convertReactions(note.reactions),
+			reactions: countReactions(note.reactions),
 			reactionEmojis: reactionEmoji,
 			emojis: noteEmoji,
 			tags: note.tags.length > 0 ? note.tags : undefined,
diff --git a/packages/backend/src/services/note/reaction/create.ts b/packages/backend/src/services/note/reaction/create.ts
index a07ffdabad..3b8b97cefd 100644
--- a/packages/backend/src/services/note/reaction/create.ts
+++ b/packages/backend/src/services/note/reaction/create.ts
@@ -2,7 +2,6 @@ import { publishNoteStream } from "@/services/stream.js";
 import { renderLike } from "@/remote/activitypub/renderer/like.js";
 import DeliverManager from "@/remote/activitypub/deliver-manager.js";
 import { renderActivity } from "@/remote/activitypub/renderer/index.js";
-import { toDbReaction, decodeReaction } from "@/misc/reaction-lib.js";
 import type { User, IRemoteUser } from "@/models/entities/user.js";
 import type { Note } from "@/models/entities/note.js";
 import {
@@ -14,7 +13,7 @@ import {
 	Blockings,
 } from "@/models/index.js";
 import { IsNull, Not } from "typeorm";
-import { genId } from "backend-rs";
+import { decodeReaction, genId, toDbReaction } from "backend-rs";
 import { createNotification } from "@/services/create-notification.js";
 import deleteReaction from "./delete.js";
 import { isDuplicateKeyValueError } from "@/misc/is-duplicate-key-value-error.js";
@@ -95,7 +94,7 @@ export default async (
 
 	const emoji = await Emojis.findOne({
 		where: {
-			name: decodedReaction.name,
+			name: decodedReaction.name ?? undefined,
 			host: decodedReaction.host ?? IsNull(),
 		},
 		select: ["name", "host", "originalUrl", "publicUrl"],
diff --git a/packages/backend/src/services/note/reaction/delete.ts b/packages/backend/src/services/note/reaction/delete.ts
index 49879a0c02..e5416a78a8 100644
--- a/packages/backend/src/services/note/reaction/delete.ts
+++ b/packages/backend/src/services/note/reaction/delete.ts
@@ -7,7 +7,7 @@ import { IdentifiableError } from "@/misc/identifiable-error.js";
 import type { User, IRemoteUser } from "@/models/entities/user.js";
 import type { Note } from "@/models/entities/note.js";
 import { NoteReactions, Users, Notes } from "@/models/index.js";
-import { decodeReaction } from "@/misc/reaction-lib.js";
+import { decodeReaction } from "backend-rs";
 
 export default async (
 	user: { id: User["id"]; host: User["host"] },

From 71c158fbd3088ab3194e39aa8a6ce71cddde257d Mon Sep 17 00:00:00 2001
From: naskya <m@naskya.net>
Date: Mon, 15 Apr 2024 17:28:20 +0900
Subject: [PATCH 066/110] refactor (backend): port env.ts to backend-rs

---
 packages/backend-rs/index.d.ts                | 10 +++++++
 packages/backend-rs/index.js                  |  3 ++-
 packages/backend-rs/src/config/environment.rs | 27 +++++++++++++++++++
 packages/backend-rs/src/config/mod.rs         |  1 +
 packages/backend/src/boot/index.ts            |  2 +-
 packages/backend/src/boot/master.ts           |  2 +-
 packages/backend/src/config/index.ts          |  2 ++
 packages/backend/src/env.ts                   | 23 ----------------
 packages/backend/src/queue/index.ts           |  2 +-
 packages/backend/src/server/index.ts          |  5 ++--
 packages/backend/src/services/logger.ts       |  3 +--
 11 files changed, 48 insertions(+), 32 deletions(-)
 create mode 100644 packages/backend-rs/src/config/environment.rs
 delete mode 100644 packages/backend/src/env.ts

diff --git a/packages/backend-rs/index.d.ts b/packages/backend-rs/index.d.ts
index a47495508e..ae050e01d8 100644
--- a/packages/backend-rs/index.d.ts
+++ b/packages/backend-rs/index.d.ts
@@ -3,6 +3,16 @@
 
 /* auto-generated by NAPI-RS */
 
+export interface EnvConfig {
+  onlyQueue: boolean
+  onlyServer: boolean
+  noDaemons: boolean
+  disableClustering: boolean
+  verbose: boolean
+  withLogTime: boolean
+  slow: boolean
+}
+export function readEnvironmentConfig(): EnvConfig
 export interface ServerConfig {
   url: string
   port: number
diff --git a/packages/backend-rs/index.js b/packages/backend-rs/index.js
index 6fca851430..6acd3ca68d 100644
--- a/packages/backend-rs/index.js
+++ b/packages/backend-rs/index.js
@@ -310,8 +310,9 @@ if (!nativeBinding) {
   throw new Error(`Failed to load native binding`)
 }
 
-const { readServerConfig, stringToAcct, acctToString, checkWordMute, getFullApAccount, isSelfHost, isSameOrigin, extractHost, toPuny, isUnicodeEmoji, sqlLikeEscape, safeForSql, formatMilliseconds, toMastodonId, fromMastodonId, fetchMeta, metaToPugArgs, nyaify, hashPassword, verifyPassword, isOldPasswordAlgorithm, decodeReaction, countReactions, toDbReaction, AntennaSrcEnum, MutedNoteReasonEnum, NoteVisibilityEnum, NotificationTypeEnum, PageVisibilityEnum, PollNotevisibilityEnum, RelayStatusEnum, UserEmojimodpermEnum, UserProfileFfvisibilityEnum, UserProfileMutingnotificationtypesEnum, initIdGenerator, getTimestamp, genId, secureRndstr } = nativeBinding
+const { readEnvironmentConfig, readServerConfig, stringToAcct, acctToString, checkWordMute, getFullApAccount, isSelfHost, isSameOrigin, extractHost, toPuny, isUnicodeEmoji, sqlLikeEscape, safeForSql, formatMilliseconds, toMastodonId, fromMastodonId, fetchMeta, metaToPugArgs, nyaify, hashPassword, verifyPassword, isOldPasswordAlgorithm, decodeReaction, countReactions, toDbReaction, AntennaSrcEnum, MutedNoteReasonEnum, NoteVisibilityEnum, NotificationTypeEnum, PageVisibilityEnum, PollNotevisibilityEnum, RelayStatusEnum, UserEmojimodpermEnum, UserProfileFfvisibilityEnum, UserProfileMutingnotificationtypesEnum, initIdGenerator, getTimestamp, genId, secureRndstr } = nativeBinding
 
+module.exports.readEnvironmentConfig = readEnvironmentConfig
 module.exports.readServerConfig = readServerConfig
 module.exports.stringToAcct = stringToAcct
 module.exports.acctToString = acctToString
diff --git a/packages/backend-rs/src/config/environment.rs b/packages/backend-rs/src/config/environment.rs
new file mode 100644
index 0000000000..7d66aec7ba
--- /dev/null
+++ b/packages/backend-rs/src/config/environment.rs
@@ -0,0 +1,27 @@
+// FIXME: Are these options used?
+#[crate::export(object)]
+pub struct EnvConfig {
+    pub only_queue: bool,
+    pub only_server: bool,
+    pub no_daemons: bool,
+    pub disable_clustering: bool,
+    pub verbose: bool,
+    pub with_log_time: bool,
+    pub slow: bool,
+}
+
+#[crate::export]
+pub fn read_environment_config() -> EnvConfig {
+    let node_env = std::env::var("NODE_ENV").unwrap_or_default().to_lowercase();
+    let is_testing = node_env == "test";
+
+    EnvConfig {
+        only_queue: std::env::var("MK_ONLY_QUEUE").is_ok(),
+        only_server: std::env::var("MK_ONLY_SERVER").is_ok(),
+        no_daemons: is_testing || std::env::var("MK_NO_DAEMONS").is_ok(),
+        disable_clustering: is_testing || std::env::var("MK_DISABLE_CLUSTERING").is_ok(),
+        verbose: std::env::var("MK_VERBOSE").is_ok(),
+        with_log_time: std::env::var("MK_WITH_LOG_TIME").is_ok(),
+        slow: std::env::var("MK_SLOW").is_ok(),
+    }
+}
diff --git a/packages/backend-rs/src/config/mod.rs b/packages/backend-rs/src/config/mod.rs
index 74f47ad347..b708f2b265 100644
--- a/packages/backend-rs/src/config/mod.rs
+++ b/packages/backend-rs/src/config/mod.rs
@@ -1 +1,2 @@
+pub mod environment;
 pub mod server;
diff --git a/packages/backend/src/boot/index.ts b/packages/backend/src/boot/index.ts
index 4ba24c280b..9854d2dce4 100644
--- a/packages/backend/src/boot/index.ts
+++ b/packages/backend/src/boot/index.ts
@@ -3,7 +3,7 @@ import chalk from "chalk";
 import Xev from "xev";
 
 import Logger from "@/services/logger.js";
-import { envOption } from "../env.js";
+import { envOption } from "@/config/index.js";
 import { inspect } from "node:util";
 
 // for typeorm
diff --git a/packages/backend/src/boot/master.ts b/packages/backend/src/boot/master.ts
index cd21c33021..3ba2d0cf50 100644
--- a/packages/backend/src/boot/master.ts
+++ b/packages/backend/src/boot/master.ts
@@ -10,7 +10,7 @@ import semver from "semver";
 import Logger from "@/services/logger.js";
 import loadConfig from "@/config/load.js";
 import type { Config } from "@/config/types.js";
-import { envOption } from "@/env.js";
+import { envOption } from "@/config/index.js";
 import { showMachineInfo } from "@/misc/show-machine-info.js";
 import { db, initDb } from "@/db/postgre.js";
 import { inspect } from "node:util";
diff --git a/packages/backend/src/config/index.ts b/packages/backend/src/config/index.ts
index ae197b09ca..fe87e5026a 100644
--- a/packages/backend/src/config/index.ts
+++ b/packages/backend/src/config/index.ts
@@ -1,3 +1,5 @@
 import load from "./load.js";
+import { readEnvironmentConfig } from "backend-rs";
 
 export default load();
+export const envOption = readEnvironmentConfig();
diff --git a/packages/backend/src/env.ts b/packages/backend/src/env.ts
deleted file mode 100644
index a10952133e..0000000000
--- a/packages/backend/src/env.ts
+++ /dev/null
@@ -1,23 +0,0 @@
-const envOption = {
-	onlyQueue: false,
-	onlyServer: false,
-	noDaemons: false,
-	disableClustering: false,
-	verbose: false,
-	withLogTime: false,
-	slow: false,
-};
-
-for (const key of Object.keys(envOption) as (keyof typeof envOption)[]) {
-	if (
-		process.env[
-			`MK_${key.replace(/[A-Z]/g, (letter) => `_${letter}`).toUpperCase()}`
-		]
-	)
-		envOption[key] = true;
-}
-
-if (process.env.NODE_ENV === "test") envOption.disableClustering = true;
-if (process.env.NODE_ENV === "test") envOption.noDaemons = true;
-
-export { envOption };
diff --git a/packages/backend/src/queue/index.ts b/packages/backend/src/queue/index.ts
index 58a0ae7486..e4e413be52 100644
--- a/packages/backend/src/queue/index.ts
+++ b/packages/backend/src/queue/index.ts
@@ -5,7 +5,7 @@ import config from "@/config/index.js";
 import type { DriveFile } from "@/models/entities/drive-file.js";
 import type { IActivity } from "@/remote/activitypub/type.js";
 import type { Webhook, webhookEventTypes } from "@/models/entities/webhook.js";
-import { envOption } from "../env.js";
+import { envOption } from "@/config/index.js";
 
 import processDeliver from "./processors/deliver.js";
 import processInbox from "./processors/inbox.js";
diff --git a/packages/backend/src/server/index.ts b/packages/backend/src/server/index.ts
index 6cf837b4ed..17358a4758 100644
--- a/packages/backend/src/server/index.ts
+++ b/packages/backend/src/server/index.ts
@@ -13,15 +13,14 @@ import koaLogger from "koa-logger";
 import * as slow from "koa-slow";
 
 import { IsNull } from "typeorm";
-import config from "@/config/index.js";
+import config, { envOption } from "@/config/index.js";
 import Logger from "@/services/logger.js";
 import { Users } from "@/models/index.js";
 import { fetchMeta } from "backend-rs";
 import { genIdenticon } from "@/misc/gen-identicon.js";
 import { createTemp } from "@/misc/create-temp.js";
 import { stringToAcct } from "backend-rs";
-import { envOption } from "@/env.js";
-import megalodon, { MegalodonInterface } from "megalodon";
+import megalodon, { type MegalodonInterface } from "megalodon";
 import activityPub from "./activitypub.js";
 import nodeinfo from "./nodeinfo.js";
 import wellKnown from "./well-known.js";
diff --git a/packages/backend/src/services/logger.ts b/packages/backend/src/services/logger.ts
index 47a1fe82f8..e53279e31c 100644
--- a/packages/backend/src/services/logger.ts
+++ b/packages/backend/src/services/logger.ts
@@ -2,8 +2,7 @@ import cluster from "node:cluster";
 import chalk from "chalk";
 import { default as convertColor } from "color-convert";
 import { format as dateFormat } from "date-fns";
-import { envOption } from "@/env.js";
-import config from "@/config/index.js";
+import config, { envOption } from "@/config/index.js";
 
 import * as SyslogPro from "syslog-pro";
 

From 80b80277e2d87588ef210867140eec3b9d4cba75 Mon Sep 17 00:00:00 2001
From: naskya <m@naskya.net>
Date: Tue, 16 Apr 2024 01:50:42 +0900
Subject: [PATCH 067/110] fix (pug): random MOTD not showing

---
 packages/backend/src/server/web/views/base.pug | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/packages/backend/src/server/web/views/base.pug b/packages/backend/src/server/web/views/base.pug
index bdbe153fbe..738d3ffc01 100644
--- a/packages/backend/src/server/web/views/base.pug
+++ b/packages/backend/src/server/web/views/base.pug
@@ -72,8 +72,8 @@ html
 		div#splash
 			img#splashIcon(src= splashIcon || `/static-assets/splash.svg?${ timestamp }`)
 			span#splashText
-				block randomMOTD
-					= randomMOTD
+				block randomMotd
+					= randomMotd
 			div#splashSpinner
 				<svg class="spinner bg" viewBox="0 0 152 152" xmlns="http://www.w3.org/2000/svg">
 					<g transform="matrix(1,0,0,1,12,12)">

From 38192052c909c4583ca245f6102806ce787504bb Mon Sep 17 00:00:00 2001
From: naskya <m@naskya.net>
Date: Tue, 16 Apr 2024 05:34:43 +0900
Subject: [PATCH 068/110] meta: update issue/merge request templates

---
 .gitlab/issue_templates/bug.md             | 32 ++++++++++++++--------
 .gitlab/issue_templates/feature.md         | 12 ++++----
 .gitlab/merge_request_templates/default.md |  4 +--
 3 files changed, 28 insertions(+), 20 deletions(-)

diff --git a/.gitlab/issue_templates/bug.md b/.gitlab/issue_templates/bug.md
index 3bffa21cde..a94fae0f0d 100644
--- a/.gitlab/issue_templates/bug.md
+++ b/.gitlab/issue_templates/bug.md
@@ -3,30 +3,38 @@
 🔒 Found a security vulnerability? [Please disclose it responsibly.](https://firefish.dev/firefish/firefish/-/blob/develop/SECURITY.md)
 🤝 By submitting this issue, you agree to follow our [Contribution Guidelines.](https://firefish.dev/firefish/firefish/-/blob/develop/CONTRIBUTING.md) -->
 
-**What happened?** _(Please give us a brief description of what happened.)_
+## What happened? <!-- Please give us a brief description of what happened. -->
 
-**What did you expect to happen?** _(Please give us a brief description of what you expected to happen.)_
+## What did you expect to happen? <!-- Please give us a brief description of what you expected to happen. -->
 
-**Version** _(What version of firefish is your instance running? You can find this by clicking your instance's logo at the bottom left and then clicking instance information.)_
+## Version <!-- What version of firefish is your instance running? You can find this by clicking your instance's logo at the bottom left and then clicking instance information. -->
 
-**Instance** _(What instance of firefish are you using?)_
+## What type of issue is this? <!-- If this happens on your device and has to do with the user interface, it's client-side. If this happens on either with the API or the backend, or you got a server-side error in the client, it's server-side. -->
 
-**What type of issue is this?** _(If this happens on your device and has to do with the user interface, it's client-side. If this happens on either with the API or the backend, or you got a server-side error in the client, it's server-side.)_
+- [] server-side
+- [] client-side
+- [] not sure
 
-**What browser are you using? (Client-side issues only)**
+<details>
 
-**What operating system are you using? (Client-side issues only)**
+### Instance <!-- What instance of firefish are you using? -->
 
-**How do you deploy Firefish on your server? (Server-side issues only)**
+### What browser are you using? (client-side issues only)
 
-**What operating system are you using? (Server-side issues only)**
+### What operating system are you using? (client-side issues only)
 
-**Relevant log output** _(Please copy and paste any relevant log output. You can find your log by inspecting the page, and going to the "console" tab. This will be automatically formatted into code, so no need for backticks.)_
+### How do you deploy Firefish on your server? (server-side issues only)
 
-**Contribution Guidelines**
+### What operating system are you using? (Server-side issues only)
+
+### Relevant log output <!-- Please copy and paste any relevant log output. You can find your log by inspecting the page, and going to the "console" tab. -->
+
+</details>
+
+## Contribution Guidelines
 By submitting this issue, you agree to follow our [Contribution Guidelines](https://firefish.dev/firefish/firefish/-/blob/develop/CONTRIBUTING.md)
 - [ ] I agree to follow this project's Contribution Guidelines
 - [ ] I have searched the issue tracker for similar issues, and this is not a duplicate.
 
-**Are you willing to fix this bug?** (optional)
+## Are you willing to fix this bug? (optional)
 - [ ] Yes. I will fix this bug and open a merge request if the change is agreed upon.
diff --git a/.gitlab/issue_templates/feature.md b/.gitlab/issue_templates/feature.md
index b4af4884b7..7b4917ecb3 100644
--- a/.gitlab/issue_templates/feature.md
+++ b/.gitlab/issue_templates/feature.md
@@ -3,18 +3,18 @@
 🔒 Found a security vulnerability? [Please disclose it responsibly.](https://firefish.dev/firefish/firefish/-/blob/develop/SECURITY.md)
 🤝 By submitting this feature request, you agree to follow our [Contribution Guidelines.](https://firefish.dev/firefish/firefish/-/blob/develop/CONTRIBUTING.md) -->
 
-**What feature would you like implemented?** _(Please give us a brief description of what you'd like.)_
+## What feature would you like implemented? <!-- Please give us a brief description of what you'd like. -->
 
-**Why should we add this feature?** _(Please give us a brief description of why your feature is important.)_
+## Why should we add this feature? <!-- Please give us a brief description of why your feature is important. -->
 
-**Version** _(What version of firefish is your instance running? You can find this by clicking your instance's logo at the bottom left and then clicking instance information.)_
+## Version <!-- What version of firefish is your instance running? You can find this by clicking your instance's logo at the bottom left and then clicking instance information. -->
 
-**Instance** _(What instance of firefish are you using?)_
+## Instance <!-- What instance of firefish are you using? -->
 
-**Contribution Guidelines**
+## Contribution Guidelines
 By submitting this issue, you agree to follow our [Contribution Guidelines](https://firefish.dev/firefish/firefish/-/blob/develop/CONTRIBUTING.md)
 - [ ] I agree to follow this project's Contribution Guidelines
 - [ ] I have searched the issue tracker for similar requests, and this is not a duplicate.
 
-**Are you willing to implement this feature?** (optional)
+## Are you willing to implement this feature? (optional)
 - [ ] Yes. I will implement this feature and open a merge request if the change is agreed upon.
diff --git a/.gitlab/merge_request_templates/default.md b/.gitlab/merge_request_templates/default.md
index 2a1c926223..c2382481e3 100644
--- a/.gitlab/merge_request_templates/default.md
+++ b/.gitlab/merge_request_templates/default.md
@@ -1,8 +1,8 @@
 <!-- Thanks for taking the time to make Firefish better! It's not required, but please consider using [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) when making your commits. If you use VSCode, please use the [Conventional Commits extension](https://marketplace.visualstudio.com/items?itemName=vivaxy.vscode-conventional-commits). -->
 
-**What does this PR do?** _(Please give us a brief description of what this PR does.)_
+## What does this PR do? <!-- Please give us a brief description of what this PR does. -->
 
-**Contribution Guidelines**
+## Contribution Guidelines
 By submitting this merge request, you agree to follow our [Contribution Guidelines](https://firefish.dev/firefish/firefish/-/blob/develop/CONTRIBUTING.md)
 - [ ] This change is reviewed in an issue / This is a minor bug fix
 - [ ] I agree to follow this project's Contribution Guidelines

From fd333250c9801fea344216778059ad8c3b3a5978 Mon Sep 17 00:00:00 2001
From: naskya <m@naskya.net>
Date: Tue, 16 Apr 2024 08:56:05 +0900
Subject: [PATCH 069/110] chore (backend): set proxyRemoteFiles to true by
 default (close #9426)

---
 .config/example.yml                 | 2 +-
 packages/backend/src/config/load.ts | 1 +
 2 files changed, 2 insertions(+), 1 deletion(-)

diff --git a/.config/example.yml b/.config/example.yml
index 9082dfb868..fdfb0b0965 100644
--- a/.config/example.yml
+++ b/.config/example.yml
@@ -178,7 +178,7 @@ logLevel: [
 # Media Proxy
 #mediaProxy: https://example.com/proxy
 
-# Proxy remote files (default: false)
+# Proxy remote files (default: true)
 #proxyRemoteFiles: true
 
 #allowedPrivateNetworks: [
diff --git a/packages/backend/src/config/load.ts b/packages/backend/src/config/load.ts
index 2b286b9439..682bf309d2 100644
--- a/packages/backend/src/config/load.ts
+++ b/packages/backend/src/config/load.ts
@@ -55,6 +55,7 @@ export default function load() {
 	mixin.userAgent = `Firefish/${meta.version} (${config.url})`;
 	mixin.clientEntry = clientManifest["src/init.ts"];
 
+	if (config.proxyRemoteFiles == null) config.proxyRemoteFiles = true;
 	if (!config.redis.prefix) config.redis.prefix = mixin.hostname;
 	if (config.cacheServer && !config.cacheServer.prefix)
 		config.cacheServer.prefix = mixin.hostname;

From 77a2bcfc4b6ba4505242e5ffb23eeaeade32dbb2 Mon Sep 17 00:00:00 2001
From: naskya <m@naskya.net>
Date: Tue, 16 Apr 2024 09:01:12 +0900
Subject: [PATCH 070/110] chore: remove unused items from example config file

---
 .config/example.yml | 6 ------
 1 file changed, 6 deletions(-)

diff --git a/.config/example.yml b/.config/example.yml
index fdfb0b0965..17149f6c3a 100644
--- a/.config/example.yml
+++ b/.config/example.yml
@@ -185,12 +185,6 @@ logLevel: [
 #  '127.0.0.1/32'
 #]
 
-# TWA
-#twa:
-#  nameSpace: android_app
-#  packageName: tld.domain.twa
-#  sha256CertFingerprints: ['AB:CD:EF']
-
 # Upload or download file size limits (bytes)
 #maxFileSize: 262144000
 

From 07384a4f0f34e61df5b4f12dfc02304c18ad8961 Mon Sep 17 00:00:00 2001
From: naskya <m@naskya.net>
Date: Tue, 16 Apr 2024 09:14:44 +0900
Subject: [PATCH 071/110] feat (backend): increase CW character limit (close
 #10876)

---
 docs/downgrade.sql                            |  6 ++++++
 packages/backend-rs/src/model/entity/note.rs  |  1 +
 ...713225866247-convert-cw-varchar-to-text.ts | 21 +++++++++++++++++++
 packages/backend/src/models/entities/note.ts  |  5 ++---
 .../src/server/api/endpoints/notes/create.ts  |  2 +-
 5 files changed, 31 insertions(+), 4 deletions(-)
 create mode 100644 packages/backend/src/migration/1713225866247-convert-cw-varchar-to-text.ts

diff --git a/docs/downgrade.sql b/docs/downgrade.sql
index 0b6f25e08c..77dea27573 100644
--- a/docs/downgrade.sql
+++ b/docs/downgrade.sql
@@ -1,6 +1,7 @@
 BEGIN;
 
 DELETE FROM "migrations" WHERE name IN (
+    'ConvertCwVarcharToText1713225866247',
     'FixChatFileConstraint1712855579316',
     'DropTimeZone1712425488543',
     'ExpandNoteEdit1711936358554',
@@ -22,6 +23,11 @@ DELETE FROM "migrations" WHERE name IN (
     'RemoveNativeUtilsMigration1705877093218'
 );
 
+--convert-cw-varchar-to-text
+DROP INDEX "IDX_8e3bbbeb3df04d1a8105da4c8f";
+ALTER TABLE "note" ALTER COLUMN "cw" TYPE character varying(512);
+CREATE INDEX "IDX_8e3bbbeb3df04d1a8105da4c8f" ON "note" USING "pgroonga" ("cw" pgroonga_varchar_full_text_search_ops_v2);
+
 -- fix-chat-file-constraint
 ALTER TABLE "messaging_message" DROP CONSTRAINT "FK_535def119223ac05ad3fa9ef64b";
 ALTER TABLE "messaging_message" ADD CONSTRAINT "FK_535def119223ac05ad3fa9ef64b" FOREIGN KEY ("fileId") REFERENCES "drive_file"("id") ON DELETE CASCADE ON UPDATE NO ACTION;
diff --git a/packages/backend-rs/src/model/entity/note.rs b/packages/backend-rs/src/model/entity/note.rs
index cb82f3d94a..5903216c1a 100644
--- a/packages/backend-rs/src/model/entity/note.rs
+++ b/packages/backend-rs/src/model/entity/note.rs
@@ -21,6 +21,7 @@ pub struct Model {
     #[sea_orm(column_type = "Text", nullable)]
     pub text: Option<String>,
     pub name: Option<String>,
+    #[sea_orm(column_type = "Text", nullable)]
     pub cw: Option<String>,
     #[sea_orm(column_name = "userId")]
     pub user_id: String,
diff --git a/packages/backend/src/migration/1713225866247-convert-cw-varchar-to-text.ts b/packages/backend/src/migration/1713225866247-convert-cw-varchar-to-text.ts
new file mode 100644
index 0000000000..93c87a98a8
--- /dev/null
+++ b/packages/backend/src/migration/1713225866247-convert-cw-varchar-to-text.ts
@@ -0,0 +1,21 @@
+import type { MigrationInterface, QueryRunner } from "typeorm";
+
+export class ConvertCwVarcharToText1713225866247 implements MigrationInterface {
+	public async up(queryRunner: QueryRunner): Promise<void> {
+		queryRunner.query(`DROP INDEX "IDX_8e3bbbeb3df04d1a8105da4c8f"`);
+		queryRunner.query(`ALTER TABLE "note" ALTER COLUMN "cw" TYPE text`);
+		queryRunner.query(
+			`CREATE INDEX "IDX_8e3bbbeb3df04d1a8105da4c8f" ON "note" USING "pgroonga" ("cw")`,
+		);
+	}
+
+	public async down(queryRunner: QueryRunner): Promise<void> {
+		queryRunner.query(`DROP INDEX "IDX_8e3bbbeb3df04d1a8105da4c8f"`);
+		queryRunner.query(
+			`ALTER TABLE "note" ALTER COLUMN "cw" TYPE character varying(512)`,
+		);
+		queryRunner.query(
+			`CREATE INDEX "IDX_8e3bbbeb3df04d1a8105da4c8f" ON "note" USING "pgroonga" ("cw" pgroonga_varchar_full_text_search_ops_v2)`,
+		);
+	}
+}
diff --git a/packages/backend/src/models/entities/note.ts b/packages/backend/src/models/entities/note.ts
index 738e43d442..94cd8c7b66 100644
--- a/packages/backend/src/models/entities/note.ts
+++ b/packages/backend/src/models/entities/note.ts
@@ -72,9 +72,8 @@ export class Note {
 	})
 	public name: string | null;
 
-	@Index() // USING pgroonga pgroonga_varchar_full_text_search_ops_v2
-	@Column("varchar", {
-		length: 512,
+	@Index() // USING pgroonga
+	@Column("text", {
 		nullable: true,
 	})
 	public cw: string | null;
diff --git a/packages/backend/src/server/api/endpoints/notes/create.ts b/packages/backend/src/server/api/endpoints/notes/create.ts
index 270c33abd0..c2302f4c8d 100644
--- a/packages/backend/src/server/api/endpoints/notes/create.ts
+++ b/packages/backend/src/server/api/endpoints/notes/create.ts
@@ -114,7 +114,7 @@ export const paramDef = {
 			enum: Object.keys(langmap),
 			nullable: true,
 		},
-		cw: { type: "string", nullable: true, maxLength: 100 },
+		cw: { type: "string", nullable: true, maxLength: MAX_NOTE_TEXT_LENGTH },
 		localOnly: { type: "boolean", default: false },
 		noExtractMentions: { type: "boolean", default: false },
 		noExtractHashtags: { type: "boolean", default: false },

From 7af2cca2d4e507e8e50ada87ea1488d407df9c17 Mon Sep 17 00:00:00 2001
From: Gary O'Regan Kelly <gmoregan@icloud.com>
Date: Mon, 15 Apr 2024 02:27:04 +0000
Subject: [PATCH 072/110] locale: update translations (French)

Currently translated at 100.0% (1920 of 1920 strings)

Translation: Firefish/locales
Translate-URL: https://hosted.weblate.org/projects/firefish/locales/fr/
---
 locales/fr-FR.yml | 1 +
 1 file changed, 1 insertion(+)

diff --git a/locales/fr-FR.yml b/locales/fr-FR.yml
index da56856077..14c46b1740 100644
--- a/locales/fr-FR.yml
+++ b/locales/fr-FR.yml
@@ -2324,3 +2324,4 @@ markLocalFilesNsfwByDefaultDescription: Indépendamment de ce réglage, les util
   peuvent supprimer le drapeau « sensible » (NSFW) eux-mêmes. Les fichiers existants
   ne sont pas affectés.
 noteEditHistory: Historique des publications
+media: Multimédia

From bf3c0717b90a69071486852e8ea0ded054e9a6d8 Mon Sep 17 00:00:00 2001
From: yumeko <yumeko@mainichi.social>
Date: Tue, 16 Apr 2024 15:29:18 +0000
Subject: [PATCH 073/110] Fix check for whether stats are disabled in meta in
 server machine stats job

---
 packages/backend/src/daemons/server-stats.ts | 7 +++----
 1 file changed, 3 insertions(+), 4 deletions(-)

diff --git a/packages/backend/src/daemons/server-stats.ts b/packages/backend/src/daemons/server-stats.ts
index 92265ecba7..df1f9b3032 100644
--- a/packages/backend/src/daemons/server-stats.ts
+++ b/packages/backend/src/daemons/server-stats.ts
@@ -13,16 +13,15 @@ const round = (num: number) => Math.round(num * 10) / 10;
 /**
  * Report server stats regularly
  */
-export default function () {
+export default async function () {
 	const log = [] as any[];
 
 	ev.on("requestServerStatsLog", (x) => {
 		ev.emit(`serverStatsLog:${x.id}`, log.slice(0, x.length || 50));
 	});
 
-	fetchMeta(true).then((meta) => {
-		if (!meta.enableServerMachineStats) return;
-	});
+	const meta = await fetchMeta(true);
+	if (!meta.enableServerMachineStats) return;
 
 	async function tick() {
 		const cpu = await cpuUsage();

From 17fb05430ede5825d7f03c9b0d74c7b57318801d Mon Sep 17 00:00:00 2001
From: naskya <m@naskya.net>
Date: Wed, 17 Apr 2024 17:46:23 +0900
Subject: [PATCH 074/110] fix (backend, Mastodon API): add 'meta.original'
 field to media attachments

addresses https://github.com/whitescent/Mastify/pull/102
---
 .../src/server/api/mastodon/converters.ts        | 16 ++++++++++++++--
 1 file changed, 14 insertions(+), 2 deletions(-)

diff --git a/packages/backend/src/server/api/mastodon/converters.ts b/packages/backend/src/server/api/mastodon/converters.ts
index b392403578..b2259e6ed5 100644
--- a/packages/backend/src/server/api/mastodon/converters.ts
+++ b/packages/backend/src/server/api/mastodon/converters.ts
@@ -1,4 +1,4 @@
-import { Entity } from "megalodon";
+import type { Entity } from "megalodon";
 import { toMastodonId } from "backend-rs";
 
 function simpleConvert(data: any) {
@@ -15,7 +15,19 @@ export function convertAnnouncement(announcement: Entity.Announcement) {
 	return simpleConvert(announcement);
 }
 export function convertAttachment(attachment: Entity.Attachment) {
-	return simpleConvert(attachment);
+	const converted = simpleConvert(attachment);
+	// ref: https://github.com/whitescent/Mastify/pull/102
+	if (converted.meta == null) return converted;
+	const result = {
+		...converted,
+		meta: {
+			...converted.meta,
+			original: {
+				...converted.meta,
+			},
+		},
+	};
+	return result;
 }
 export function convertFilter(filter: Entity.Filter) {
 	return simpleConvert(filter);

From 84890661307ee6177639bf4a7f2ab961059b7c75 Mon Sep 17 00:00:00 2001
From: naskya <m@naskya.net>
Date: Wed, 17 Apr 2024 19:06:29 +0900
Subject: [PATCH 075/110] fix (client): list layout on mobile

---
 .../client/src/pages/user-list-timeline.vue   | 21 ++++++++-----------
 1 file changed, 9 insertions(+), 12 deletions(-)

diff --git a/packages/client/src/pages/user-list-timeline.vue b/packages/client/src/pages/user-list-timeline.vue
index 69f17c1047..92a25ddd8d 100644
--- a/packages/client/src/pages/user-list-timeline.vue
+++ b/packages/client/src/pages/user-list-timeline.vue
@@ -3,7 +3,7 @@
 		<template #header
 			><MkPageHeader :actions="headerActions" :tabs="headerTabs"
 		/></template>
-		<div ref="rootEl" class="eqqrhokj">
+		<MkSpacer>
 			<div class="tl _block">
 				<XTimeline
 					ref="tlEl"
@@ -14,12 +14,15 @@
 					:sound="true"
 				/>
 			</div>
-		</div>
+		</MkSpacer>
 	</MkStickyContainer>
 </template>
 
 <script lang="ts" setup>
 import { computed, ref, watch } from "vue";
+import type { entities } from "firefish-js";
+// TODO: disable this rule properly
+// biome-ignore lint/style/useImportType: used in <template>
 import XTimeline from "@/components/MkTimeline.vue";
 import * as os from "@/os";
 import { useRouter } from "@/router";
@@ -33,9 +36,8 @@ const props = defineProps<{
 	listId: string;
 }>();
 
-const list = ref(null);
+const list = ref<entities.UserList>();
 const tlEl = ref<InstanceType<typeof XTimeline>>();
-const rootEl = ref<HTMLElement>();
 
 watch(
 	() => props.listId,
@@ -92,13 +94,8 @@ definePageMetadata(
 </script>
 
 <style lang="scss" scoped>
-.eqqrhokj {
-	padding: var(--margin);
-	max-width: 800px;
-	margin: 0 auto;
-	> .tl {
-		background: none;
-		border-radius: var(--radius);
-	}
+.tl {
+	background: none;
+	border-radius: var(--radius);
 }
 </style>

From ec7578e78e652c2f2a152c4945d678f067ab48c2 Mon Sep 17 00:00:00 2001
From: naskya <m@naskya.net>
Date: Wed, 17 Apr 2024 19:49:02 +0900
Subject: [PATCH 076/110] docs: specify max-old-space-size in example config
 files

---
 docker-compose.example.yml | 1 +
 docs/install.md            | 3 ++-
 2 files changed, 3 insertions(+), 1 deletion(-)

diff --git a/docker-compose.example.yml b/docker-compose.example.yml
index fc6c0268a2..9cd6d1cdce 100644
--- a/docker-compose.example.yml
+++ b/docker-compose.example.yml
@@ -17,6 +17,7 @@ services:
 #     - web
     environment:
       NODE_ENV: production
+      NODE_OPTIONS: --max-old-space-size=3072
     volumes:
       - ./custom:/firefish/custom:ro
       - ./files:/firefish/files
diff --git a/docs/install.md b/docs/install.md
index 061000fa32..324923c6a7 100644
--- a/docs/install.md
+++ b/docs/install.md
@@ -154,7 +154,7 @@ sudo apt install ffmpeg
 1. Build
     ```sh
     pnpm install --frozen-lockfile
-    NODE_ENV=production pnpm run build
+    NODE_ENV=production NODE_OPTIONS='--max-old-space-size=3072' pnpm run build
     ```
 1. Execute database migrations
     ```sh
@@ -242,6 +242,7 @@ In this instruction, we use [Caddy](https://caddyserver.com/) to make the Firefi
     WorkingDirectory=/home/firefish/firefish
     Environment="NODE_ENV=production"
     Environment="npm_config_cache=/tmp"
+		Environment="NODE_OPTIONS=--max-old-space-size=3072"
     # uncomment the following line if you use jemalloc (note that the path varies on different environments)
     # Environment="LD_PRELOAD=/usr/lib/x86_64-linux-gnu/libjemalloc.so.2"
     StandardOutput=journal

From 22f4278ab5ee7a66cdaf3bbc234301b165a8c166 Mon Sep 17 00:00:00 2001
From: naskya <m@naskya.net>
Date: Wed, 17 Apr 2024 23:14:43 +0900
Subject: [PATCH 077/110] meta: update issue/merge request templates

---
 .gitlab/issue_templates/bug.md             | 15 ++++++++++++---
 .gitlab/issue_templates/feature.md         |  4 ++++
 .gitlab/merge_request_templates/default.md |  1 +
 3 files changed, 17 insertions(+), 3 deletions(-)

diff --git a/.gitlab/issue_templates/bug.md b/.gitlab/issue_templates/bug.md
index a94fae0f0d..f96bdec01e 100644
--- a/.gitlab/issue_templates/bug.md
+++ b/.gitlab/issue_templates/bug.md
@@ -5,30 +5,39 @@
 
 ## What happened? <!-- Please give us a brief description of what happened. -->
 
+
 ## What did you expect to happen? <!-- Please give us a brief description of what you expected to happen. -->
 
+
 ## Version <!-- What version of firefish is your instance running? You can find this by clicking your instance's logo at the bottom left and then clicking instance information. -->
 
+
 ## What type of issue is this? <!-- If this happens on your device and has to do with the user interface, it's client-side. If this happens on either with the API or the backend, or you got a server-side error in the client, it's server-side. -->
 
-- [] server-side
-- [] client-side
-- [] not sure
+- [ ] server-side
+- [ ] client-side
+- [ ] not sure
 
 <details>
 
 ### Instance <!-- What instance of firefish are you using? -->
 
+
 ### What browser are you using? (client-side issues only)
 
+
 ### What operating system are you using? (client-side issues only)
 
+
 ### How do you deploy Firefish on your server? (server-side issues only)
 
+
 ### What operating system are you using? (Server-side issues only)
 
+
 ### Relevant log output <!-- Please copy and paste any relevant log output. You can find your log by inspecting the page, and going to the "console" tab. -->
 
+
 </details>
 
 ## Contribution Guidelines
diff --git a/.gitlab/issue_templates/feature.md b/.gitlab/issue_templates/feature.md
index 7b4917ecb3..4c9ee56226 100644
--- a/.gitlab/issue_templates/feature.md
+++ b/.gitlab/issue_templates/feature.md
@@ -5,12 +5,16 @@
 
 ## What feature would you like implemented? <!-- Please give us a brief description of what you'd like. -->
 
+
 ## Why should we add this feature? <!-- Please give us a brief description of why your feature is important. -->
 
+
 ## Version <!-- What version of firefish is your instance running? You can find this by clicking your instance's logo at the bottom left and then clicking instance information. -->
 
+
 ## Instance <!-- What instance of firefish are you using? -->
 
+
 ## Contribution Guidelines
 By submitting this issue, you agree to follow our [Contribution Guidelines](https://firefish.dev/firefish/firefish/-/blob/develop/CONTRIBUTING.md)
 - [ ] I agree to follow this project's Contribution Guidelines
diff --git a/.gitlab/merge_request_templates/default.md b/.gitlab/merge_request_templates/default.md
index c2382481e3..d13a146da0 100644
--- a/.gitlab/merge_request_templates/default.md
+++ b/.gitlab/merge_request_templates/default.md
@@ -2,6 +2,7 @@
 
 ## What does this PR do? <!-- Please give us a brief description of what this PR does. -->
 
+
 ## Contribution Guidelines
 By submitting this merge request, you agree to follow our [Contribution Guidelines](https://firefish.dev/firefish/firefish/-/blob/develop/CONTRIBUTING.md)
 - [ ] This change is reviewed in an issue / This is a minor bug fix

From a411f4e4d9ee61ca68ff286d6c9e543d5f719fc7 Mon Sep 17 00:00:00 2001
From: yumeko <yumeko@mainichi.social>
Date: Wed, 17 Apr 2024 18:19:17 +0000
Subject: [PATCH 078/110] Fix internal error in api/pinned-users if one or more
 name fails to resolve

---
 packages/backend/src/server/api/endpoints/pinned-users.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/packages/backend/src/server/api/endpoints/pinned-users.ts b/packages/backend/src/server/api/endpoints/pinned-users.ts
index 325b54f350..5685b698eb 100644
--- a/packages/backend/src/server/api/endpoints/pinned-users.ts
+++ b/packages/backend/src/server/api/endpoints/pinned-users.ts
@@ -45,7 +45,7 @@ export default define(meta, paramDef, async (ps, me) => {
 	);
 
 	return await Users.packMany(
-		users.filter((x) => x !== undefined) as User[],
+		users.filter((x) => (x != null)) as User[],
 		me,
 		{ detail: true },
 	);

From 8337863ed3da565224a966d2489225f485015dfb Mon Sep 17 00:00:00 2001
From: naskya <m@naskya.net>
Date: Thu, 18 Apr 2024 05:01:49 +0900
Subject: [PATCH 079/110] chore: format

---
 packages/backend/src/server/api/endpoints/pinned-users.ts | 8 +++-----
 1 file changed, 3 insertions(+), 5 deletions(-)

diff --git a/packages/backend/src/server/api/endpoints/pinned-users.ts b/packages/backend/src/server/api/endpoints/pinned-users.ts
index 5685b698eb..65241becae 100644
--- a/packages/backend/src/server/api/endpoints/pinned-users.ts
+++ b/packages/backend/src/server/api/endpoints/pinned-users.ts
@@ -44,9 +44,7 @@ export default define(meta, paramDef, async (ps, me) => {
 			),
 	);
 
-	return await Users.packMany(
-		users.filter((x) => (x != null)) as User[],
-		me,
-		{ detail: true },
-	);
+	return await Users.packMany(users.filter((x) => x != null) as User[], me, {
+		detail: true,
+	});
 });

From 30969ad81799b7688ae3565066a57ad4d2dee83d Mon Sep 17 00:00:00 2001
From: naskya <m@naskya.net>
Date: Thu, 18 Apr 2024 05:02:00 +0900
Subject: [PATCH 080/110] refactor (backend): port get-note-summary to
 backend-rs

I removed trim() as it wasn't strictly neccessary
---
 packages/backend-rs/index.d.ts                | 13 ++-
 packages/backend-rs/index.js                  |  3 +-
 .../backend-rs/src/misc/check_word_mute.rs    |  3 +-
 .../backend-rs/src/misc/get_note_summary.rs   | 90 +++++++++++++++++++
 packages/backend-rs/src/misc/mod.rs           |  1 +
 packages/backend/src/misc/get-note-summary.ts | 53 -----------
 packages/backend/src/models/schema/note.ts    |  9 +-
 packages/backend/src/server/web/index.ts      | 11 ++-
 .../backend/src/services/push-notification.ts | 11 ++-
 9 files changed, 123 insertions(+), 71 deletions(-)
 create mode 100644 packages/backend-rs/src/misc/get_note_summary.rs
 delete mode 100644 packages/backend/src/misc/get-note-summary.ts

diff --git a/packages/backend-rs/index.d.ts b/packages/backend-rs/index.d.ts
index ae050e01d8..5ee69969c6 100644
--- a/packages/backend-rs/index.d.ts
+++ b/packages/backend-rs/index.d.ts
@@ -128,7 +128,8 @@ export interface Acct {
 }
 export function stringToAcct(acct: string): Acct
 export function acctToString(acct: Acct): string
-export interface NoteLike {
+/** TODO: handle name collisions better */
+export interface NoteLikeForCheckWordMute {
   fileIds: Array<string>
   userId: string | null
   text: string | null
@@ -136,7 +137,7 @@ export interface NoteLike {
   renoteId: string | null
   replyId: string | null
 }
-export function checkWordMute(note: NoteLike, mutedWordLists: Array<Array<string>>, mutedPatterns: Array<string>): Promise<boolean>
+export function checkWordMute(note: NoteLikeForCheckWordMute, mutedWordLists: Array<Array<string>>, mutedPatterns: Array<string>): Promise<boolean>
 export function getFullApAccount(username: string, host?: string | undefined | null): string
 export function isSelfHost(host?: string | undefined | null): boolean
 export function isSameOrigin(uri: string): boolean
@@ -147,6 +148,14 @@ export function sqlLikeEscape(src: string): string
 export function safeForSql(src: string): boolean
 /** Convert milliseconds to a human readable string */
 export function formatMilliseconds(milliseconds: number): string
+/** TODO: handle name collisions better */
+export interface NoteLikeForGetNoteSummary {
+  fileIds: Array<string>
+  text: string | null
+  cw: string | null
+  hasPoll: boolean
+}
+export function getNoteSummary(note: NoteLikeForGetNoteSummary): string
 export function toMastodonId(firefishId: string): string | null
 export function fromMastodonId(mastodonId: string): string | null
 export function fetchMeta(useCache: boolean): Promise<Meta>
diff --git a/packages/backend-rs/index.js b/packages/backend-rs/index.js
index 6acd3ca68d..1ea7bb5bed 100644
--- a/packages/backend-rs/index.js
+++ b/packages/backend-rs/index.js
@@ -310,7 +310,7 @@ if (!nativeBinding) {
   throw new Error(`Failed to load native binding`)
 }
 
-const { readEnvironmentConfig, readServerConfig, stringToAcct, acctToString, checkWordMute, getFullApAccount, isSelfHost, isSameOrigin, extractHost, toPuny, isUnicodeEmoji, sqlLikeEscape, safeForSql, formatMilliseconds, toMastodonId, fromMastodonId, fetchMeta, metaToPugArgs, nyaify, hashPassword, verifyPassword, isOldPasswordAlgorithm, decodeReaction, countReactions, toDbReaction, AntennaSrcEnum, MutedNoteReasonEnum, NoteVisibilityEnum, NotificationTypeEnum, PageVisibilityEnum, PollNotevisibilityEnum, RelayStatusEnum, UserEmojimodpermEnum, UserProfileFfvisibilityEnum, UserProfileMutingnotificationtypesEnum, initIdGenerator, getTimestamp, genId, secureRndstr } = nativeBinding
+const { readEnvironmentConfig, readServerConfig, stringToAcct, acctToString, checkWordMute, getFullApAccount, isSelfHost, isSameOrigin, extractHost, toPuny, isUnicodeEmoji, sqlLikeEscape, safeForSql, formatMilliseconds, getNoteSummary, toMastodonId, fromMastodonId, fetchMeta, metaToPugArgs, nyaify, hashPassword, verifyPassword, isOldPasswordAlgorithm, decodeReaction, countReactions, toDbReaction, AntennaSrcEnum, MutedNoteReasonEnum, NoteVisibilityEnum, NotificationTypeEnum, PageVisibilityEnum, PollNotevisibilityEnum, RelayStatusEnum, UserEmojimodpermEnum, UserProfileFfvisibilityEnum, UserProfileMutingnotificationtypesEnum, initIdGenerator, getTimestamp, genId, secureRndstr } = nativeBinding
 
 module.exports.readEnvironmentConfig = readEnvironmentConfig
 module.exports.readServerConfig = readServerConfig
@@ -326,6 +326,7 @@ module.exports.isUnicodeEmoji = isUnicodeEmoji
 module.exports.sqlLikeEscape = sqlLikeEscape
 module.exports.safeForSql = safeForSql
 module.exports.formatMilliseconds = formatMilliseconds
+module.exports.getNoteSummary = getNoteSummary
 module.exports.toMastodonId = toMastodonId
 module.exports.fromMastodonId = fromMastodonId
 module.exports.fetchMeta = fetchMeta
diff --git a/packages/backend-rs/src/misc/check_word_mute.rs b/packages/backend-rs/src/misc/check_word_mute.rs
index 801175c2af..18b550c29b 100644
--- a/packages/backend-rs/src/misc/check_word_mute.rs
+++ b/packages/backend-rs/src/misc/check_word_mute.rs
@@ -4,7 +4,8 @@ use once_cell::sync::Lazy;
 use regex::Regex;
 use sea_orm::{prelude::*, QuerySelect};
 
-#[crate::export(object)]
+/// TODO: handle name collisions better
+#[crate::export(object, js_name = "NoteLikeForCheckWordMute")]
 pub struct NoteLike {
     pub file_ids: Vec<String>,
     pub user_id: Option<String>,
diff --git a/packages/backend-rs/src/misc/get_note_summary.rs b/packages/backend-rs/src/misc/get_note_summary.rs
new file mode 100644
index 0000000000..3b759b04f5
--- /dev/null
+++ b/packages/backend-rs/src/misc/get_note_summary.rs
@@ -0,0 +1,90 @@
+/// TODO: handle name collisions better
+#[crate::export(object, js_name = "NoteLikeForGetNoteSummary")]
+pub struct NoteLike {
+    pub file_ids: Vec<String>,
+    pub text: Option<String>,
+    pub cw: Option<String>,
+    pub has_poll: bool,
+}
+
+#[crate::export]
+pub fn get_note_summary(note: NoteLike) -> String {
+    let mut buf: Vec<String> = vec![];
+
+    if let Some(cw) = note.cw {
+        buf.push(cw)
+    } else if let Some(text) = note.text {
+        buf.push(text)
+    }
+
+    match note.file_ids.len() {
+        0 => (),
+        1 => buf.push("📎".to_string()),
+        n => buf.push(format!("📎 ({})", n)),
+    };
+
+    if note.has_poll {
+        buf.push("📊".to_string())
+    }
+
+    buf.join(" ")
+}
+
+#[cfg(test)]
+mod unit_test {
+    use super::{get_note_summary, NoteLike};
+    use pretty_assertions::assert_eq;
+
+    #[test]
+    fn test_note_summary() {
+        let note = NoteLike {
+            file_ids: vec![],
+            text: Some("Hello world!".to_string()),
+            cw: None,
+            has_poll: false,
+        };
+        assert_eq!(get_note_summary(note), "Hello world!");
+
+        let note_with_cw = NoteLike {
+            file_ids: vec![],
+            text: Some("Hello world!".to_string()),
+            cw: Some("Content warning".to_string()),
+            has_poll: false,
+        };
+        assert_eq!(get_note_summary(note_with_cw), "Content warning");
+
+        let note_with_file_and_cw = NoteLike {
+            file_ids: vec!["9s7fmcqogiq4igin".to_string()],
+            text: None,
+            cw: Some("Selfie, no ec".to_string()),
+            has_poll: false,
+        };
+        assert_eq!(get_note_summary(note_with_file_and_cw), "Selfie, no ec 📎");
+
+        let note_with_files_only = NoteLike {
+            file_ids: vec![
+                "9s7fmcqogiq4igin".to_string(),
+                "9s7qrld5u14cey98".to_string(),
+                "9s7gebs5zgts4kca".to_string(),
+                "9s5z3e4vefqd29ee".to_string(),
+            ],
+            text: None,
+            cw: None,
+            has_poll: false,
+        };
+        assert_eq!(get_note_summary(note_with_files_only), "📎 (4)");
+
+        let note_all = NoteLike {
+            file_ids: vec![
+                "9s7fmcqogiq4igin".to_string(),
+                "9s7qrld5u14cey98".to_string(),
+                "9s7gebs5zgts4kca".to_string(),
+                "9s5z3e4vefqd29ee".to_string(),
+            ],
+            text: Some("Hello world!".to_string()),
+            cw: Some("Content warning".to_string()),
+            has_poll: true,
+        };
+        assert_eq!(get_note_summary(note_all), "Content warning 📎 (4) 📊");
+    }
+}
diff --git a/packages/backend-rs/src/misc/mod.rs b/packages/backend-rs/src/misc/mod.rs
index 45fd31cdcd..a9d7074dbf 100644
--- a/packages/backend-rs/src/misc/mod.rs
+++ b/packages/backend-rs/src/misc/mod.rs
@@ -4,6 +4,7 @@ pub mod convert_host;
 pub mod emoji;
 pub mod escape_sql;
 pub mod format_milliseconds;
+pub mod get_note_summary;
 pub mod mastodon_id;
 pub mod meta;
 pub mod nyaify;
diff --git a/packages/backend/src/misc/get-note-summary.ts b/packages/backend/src/misc/get-note-summary.ts
deleted file mode 100644
index 0a662e434e..0000000000
--- a/packages/backend/src/misc/get-note-summary.ts
+++ /dev/null
@@ -1,53 +0,0 @@
-import type { Packed } from "./schema.js";
-
-/**
- * 投稿を表す文字列を取得します。
- * @param {*} note (packされた)投稿
- */
-export const getNoteSummary = (note: Packed<"Note">): string => {
-	if (note.deletedAt) {
-		return "❌";
-	}
-
-	let summary = "";
-
-	// 本文
-	if (note.cw != null) {
-		summary += note.cw;
-	} else {
-		summary += note.text ? note.text : "";
-	}
-
-	// ファイルが添付されているとき
-	if ((note.files || []).length !== 0) {
-		const len = note.files?.length;
-		summary += ` 📎${len !== 1 ? ` (${len})` : ""}`;
-	}
-
-	// 投票が添付されているとき
-	if (note.poll) {
-		summary += " 📊";
-	}
-
-	/*
-	// 返信のとき
-	if (note.replyId) {
-		if (note.reply) {
-			summary += `\n\nRE: ${getNoteSummary(note.reply)}`;
-		} else {
-			summary += '\n\nRE: ...';
-		}
-	}
-
-	// Renoteのとき
-	if (note.renoteId) {
-		if (note.renote) {
-			summary += `\n\nRN: ${getNoteSummary(note.renote)}`;
-		} else {
-			summary += '\n\nRN: ...';
-		}
-	}
-	*/
-
-	return summary.trim();
-};
diff --git a/packages/backend/src/models/schema/note.ts b/packages/backend/src/models/schema/note.ts
index 7dcdbc9b03..fff872b69f 100644
--- a/packages/backend/src/models/schema/note.ts
+++ b/packages/backend/src/models/schema/note.ts
@@ -28,7 +28,7 @@ export const packedNoteSchema = {
 		},
 		cw: {
 			type: "string",
-			optional: true,
+			optional: false,
 			nullable: true,
 		},
 		userId: {
@@ -98,7 +98,7 @@ export const packedNoteSchema = {
 		},
 		fileIds: {
 			type: "array",
-			optional: true,
+			optional: false,
 			nullable: false,
 			items: {
 				type: "string",
@@ -128,6 +128,11 @@ export const packedNoteSchema = {
 				nullable: false,
 			},
 		},
+		hasPoll: {
+			type: "boolean",
+			optional: false,
+			nullable: false,
+		},
 		poll: {
 			type: "object",
 			optional: true,
diff --git a/packages/backend/src/server/web/index.ts b/packages/backend/src/server/web/index.ts
index 6473073370..939fcfab14 100644
--- a/packages/backend/src/server/web/index.ts
+++ b/packages/backend/src/server/web/index.ts
@@ -27,8 +27,7 @@ import {
 	Emojis,
 	GalleryPosts,
 } from "@/models/index.js";
-import { stringToAcct } from "backend-rs";
-import { getNoteSummary } from "@/misc/get-note-summary.js";
+import { getNoteSummary, stringToAcct } from "backend-rs";
 import { queues } from "@/queue/queues.js";
 import { genOpenapiSpec } from "../api/openapi/gen-spec.js";
 import { urlPreviewHandler } from "./url-preview.js";
@@ -517,8 +516,8 @@ router.get("/notes/:note", async (ctx, next) => {
 	});
 
 	try {
-		if (note) {
-			const _note = await Notes.pack(note);
+		if (note != null) {
+			const packedNote = await Notes.pack(note);
 
 			const profile = await UserProfiles.findOneByOrFail({
 				userId: note.userId,
@@ -526,13 +525,13 @@ router.get("/notes/:note", async (ctx, next) => {
 			const meta = await fetchMeta(true);
 			await ctx.render("note", {
 				...metaToPugArgs(meta),
-				note: _note,
+				note: packedNote,
 				profile,
 				avatarUrl: await Users.getAvatarUrl(
 					await Users.findOneByOrFail({ id: note.userId }),
 				),
 				// TODO: Let locale changeable by instance setting
-				summary: getNoteSummary(_note),
+				summary: getNoteSummary(note),
 			});
 
 			ctx.set("Cache-Control", "public, max-age=15");
diff --git a/packages/backend/src/services/push-notification.ts b/packages/backend/src/services/push-notification.ts
index 1a772ff9c5..3f1f2cfb1a 100644
--- a/packages/backend/src/services/push-notification.ts
+++ b/packages/backend/src/services/push-notification.ts
@@ -1,9 +1,8 @@
 import push from "web-push";
 import config from "@/config/index.js";
 import { SwSubscriptions } from "@/models/index.js";
-import { fetchMeta } from "backend-rs";
+import { fetchMeta, getNoteSummary } from "backend-rs";
 import type { Packed } from "@/misc/schema.js";
-import { getNoteSummary } from "@/misc/get-note-summary.js";
 
 // Defined also packages/sw/types.ts#L14-L21
 type pushNotificationsTypes = {
@@ -17,15 +16,15 @@ type pushNotificationsTypes = {
 
 // プッシュメッセージサーバーには文字数制限があるため、内容を削減します
 function truncateNotification(notification: Packed<"Notification">): any {
-	if (notification.note) {
+	if (notification.note != null) {
 		return {
 			...notification,
 			note: {
 				...notification.note,
-				// textをgetNoteSummaryしたものに置き換える
+				// replace the text with summary
 				text: getNoteSummary(
-					notification.type === "renote"
-						? (notification.note.renote as Packed<"Note">)
+					notification.type === "renote" && notification.note.renote != null
+						? notification.note.renote
 						: notification.note,
 				),
 

From c19c439ac11692efd15e5fade5bed13ae52f9886 Mon Sep 17 00:00:00 2001
From: naskya <m@naskya.net>
Date: Thu, 18 Apr 2024 05:08:14 +0900
Subject: [PATCH 081/110] fix (backend): add hasPoll to packed note

---
 packages/backend/src/models/repositories/note.ts | 1 +
 1 file changed, 1 insertion(+)

diff --git a/packages/backend/src/models/repositories/note.ts b/packages/backend/src/models/repositories/note.ts
index 1bf4ef657f..c877048709 100644
--- a/packages/backend/src/models/repositories/note.ts
+++ b/packages/backend/src/models/repositories/note.ts
@@ -232,6 +232,7 @@ export const NoteRepository = db.getRepository(Note).extend({
 			uri: note.uri || undefined,
 			url: note.url || undefined,
 			updatedAt: note.updatedAt?.toISOString() || undefined,
+			hasPoll: note.hasPoll,
 			poll: note.hasPoll ? populatePoll(note, meId) : undefined,
 			...(meId
 				? {

From ff08d044b574aaa1ba36922bf97d64f461067641 Mon Sep 17 00:00:00 2001
From: naskya <m@naskya.net>
Date: Thu, 18 Apr 2024 05:22:54 +0900
Subject: [PATCH 082/110] chore: format

---
 packages/client/src/components/MkMenu.vue | 2 +-
 packages/client/src/os.ts                 | 8 ++++++--
 2 files changed, 7 insertions(+), 3 deletions(-)

diff --git a/packages/client/src/components/MkMenu.vue b/packages/client/src/components/MkMenu.vue
index 1391024264..a90e42fc29 100644
--- a/packages/client/src/components/MkMenu.vue
+++ b/packages/client/src/components/MkMenu.vue
@@ -244,7 +244,7 @@ const itemsEl = ref<HTMLDivElement>();
 
 /**
  * Strictly speaking, this type conversion is wrong
- * because `ref` will deeply unpack the `ref` in `MenuSwitch`.  
+ * because `ref` will deeply unpack the `ref` in `MenuSwitch`.
  * But it performs correctly, so who cares?
  */
 const items2 = ref([]) as Ref<InnerMenuItem[]>;
diff --git a/packages/client/src/os.ts b/packages/client/src/os.ts
index 0bdacd3adb..8fbe8b8041 100644
--- a/packages/client/src/os.ts
+++ b/packages/client/src/os.ts
@@ -841,7 +841,9 @@ export async function openEmojiPicker(
 
 	activeTextarea = initialTextarea;
 
-	const textareas = document.querySelectorAll<HTMLTextAreaElement | HTMLInputElement>("textarea, input");
+	const textareas = document.querySelectorAll<
+		HTMLTextAreaElement | HTMLInputElement
+	>("textarea, input");
 	for (const textarea of Array.from(textareas)) {
 		textarea.addEventListener("focus", () => {
 			activeTextarea = textarea;
@@ -853,7 +855,9 @@ export async function openEmojiPicker(
 			for (const node of Array.from(record.addedNodes).filter(
 				(node) => node instanceof HTMLElement,
 			) as HTMLElement[]) {
-				const textareas = node.querySelectorAll<HTMLTextAreaElement | HTMLInputElement>("textarea, input");
+				const textareas = node.querySelectorAll<
+					HTMLTextAreaElement | HTMLInputElement
+				>("textarea, input");
 				for (const textarea of Array.from(textareas).filter(
 					(textarea) => textarea.dataset.preventEmojiInsert == null,
 				)) {

From 78092cd4bedf339176ba367580bd8f5c7dc9c960 Mon Sep 17 00:00:00 2001
From: naskya <m@naskya.net>
Date: Thu, 18 Apr 2024 05:32:42 +0900
Subject: [PATCH 083/110] dev (client): update eslint rules

---
 packages/client/.eslintrc.json | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/packages/client/.eslintrc.json b/packages/client/.eslintrc.json
index b0e97b2fa6..37d80f6588 100644
--- a/packages/client/.eslintrc.json
+++ b/packages/client/.eslintrc.json
@@ -4,10 +4,10 @@
 	"ignorePatterns": ["**/*.json5"],
 	"rules": {
 		"file-progress/activate": 1,
-		"prettier/prettier": 0,
-		"one-var": ["error", "never"],
+		"prettier/prettier": "off",
+		"one-var": ["warn", "never"],
 		"@typescript-eslint/no-unused-vars": [
-			"error",
+			"warn",
 			{
 				"argsIgnorePattern": "^_",
 				"varsIgnorePattern": "^_",

From 9d876798004f3dcbe812cab2c08cfc221625f910 Mon Sep 17 00:00:00 2001
From: naskya <m@naskya.net>
Date: Thu, 18 Apr 2024 05:34:23 +0900
Subject: [PATCH 084/110] chore: lint

---
 packages/client/src/components/MkAbuseReport.vue            | 2 +-
 packages/client/src/components/MkAnnouncement.vue           | 2 +-
 packages/client/src/components/MkAvatars.vue                | 2 +-
 packages/client/src/components/MkChannelFollowButton.vue    | 2 +-
 packages/client/src/components/MkChannelPreview.vue         | 2 +-
 packages/client/src/components/MkContainer.vue              | 2 +-
 packages/client/src/components/MkGalleryPostPreview.vue     | 2 +-
 packages/client/src/components/MkInstanceStats.vue          | 2 +-
 packages/client/src/components/MkInstanceTicker.vue         | 2 +-
 packages/client/src/components/MkMediaCaption.vue           | 2 +-
 packages/client/src/components/MkNotes.vue                  | 2 +-
 packages/client/src/components/MkNotification.vue           | 4 ++--
 packages/client/src/components/MkNotificationToast.vue      | 2 +-
 packages/client/src/components/MkPagePreview.vue            | 2 +-
 packages/client/src/components/MkReactionTooltip.vue        | 2 +-
 .../client/src/components/MkReactionsViewer.details.vue     | 4 ++--
 packages/client/src/components/MkSignin.vue                 | 2 +-
 packages/client/src/components/MkUserList.vue               | 2 +-
 packages/client/src/components/MkUsersTooltip.vue           | 4 ++--
 packages/client/src/components/MkWaitingDialog.vue          | 3 ++-
 packages/client/src/os.ts                                   | 6 +++---
 packages/client/src/pages/admin/abuses.vue                  | 2 +-
 packages/client/src/pages/channel-editor.vue                | 2 +-
 packages/client/src/pages/channel.vue                       | 2 +-
 packages/client/src/pages/channels.vue                      | 2 +-
 packages/client/src/pages/gallery/post.vue                  | 2 +-
 packages/client/src/pages/my-antennas/index.vue             | 3 ++-
 packages/client/src/pages/note-history.vue                  | 2 +-
 packages/client/src/scripts/use-chart-tooltip.ts            | 6 +++---
 packages/client/src/store.ts                                | 2 +-
 packages/client/src/types/form.ts                           | 4 ++--
 packages/client/src/types/note.ts                           | 4 ++--
 packages/client/src/types/post-form.ts                      | 4 ++--
 33 files changed, 45 insertions(+), 43 deletions(-)

diff --git a/packages/client/src/components/MkAbuseReport.vue b/packages/client/src/components/MkAbuseReport.vue
index 26b91fae2e..b190652052 100644
--- a/packages/client/src/components/MkAbuseReport.vue
+++ b/packages/client/src/components/MkAbuseReport.vue
@@ -67,12 +67,12 @@
 <script lang="ts" setup>
 import { ref } from "vue";
 
+import type { entities } from "firefish-js";
 import MkButton from "@/components/MkButton.vue";
 import MkSwitch from "@/components/form/switch.vue";
 import MkKeyValue from "@/components/MkKeyValue.vue";
 import * as os from "@/os";
 import { i18n } from "@/i18n";
-import type { entities } from "firefish-js";
 
 const props = defineProps<{
 	report: entities.AbuseUserReport;
diff --git a/packages/client/src/components/MkAnnouncement.vue b/packages/client/src/components/MkAnnouncement.vue
index 0aaa519cb0..3af8e0163f 100644
--- a/packages/client/src/components/MkAnnouncement.vue
+++ b/packages/client/src/components/MkAnnouncement.vue
@@ -30,12 +30,12 @@
 
 <script lang="ts" setup>
 import { shallowRef } from "vue";
+import type { entities } from "firefish-js";
 import MkModal from "@/components/MkModal.vue";
 import MkSparkle from "@/components/MkSparkle.vue";
 import MkButton from "@/components/MkButton.vue";
 import { i18n } from "@/i18n";
 import * as os from "@/os";
-import type { entities } from "firefish-js";
 
 const props = defineProps<{
 	announcement: entities.Announcement;
diff --git a/packages/client/src/components/MkAvatars.vue b/packages/client/src/components/MkAvatars.vue
index 2b2029a8b5..c1edf1e391 100644
--- a/packages/client/src/components/MkAvatars.vue
+++ b/packages/client/src/components/MkAvatars.vue
@@ -8,8 +8,8 @@
 
 <script lang="ts" setup>
 import { onMounted, ref } from "vue";
-import * as os from "@/os";
 import type { entities } from "firefish-js";
+import * as os from "@/os";
 
 const props = defineProps<{
 	userIds: string[];
diff --git a/packages/client/src/components/MkChannelFollowButton.vue b/packages/client/src/components/MkChannelFollowButton.vue
index 98e239195c..3ff907b25b 100644
--- a/packages/client/src/components/MkChannelFollowButton.vue
+++ b/packages/client/src/components/MkChannelFollowButton.vue
@@ -24,10 +24,10 @@
 
 <script lang="ts" setup>
 import { ref } from "vue";
+import type { entities } from "firefish-js";
 import * as os from "@/os";
 import { i18n } from "@/i18n";
 import icon from "@/scripts/icon";
-import type { entities } from "firefish-js";
 
 const props = withDefaults(
 	defineProps<{
diff --git a/packages/client/src/components/MkChannelPreview.vue b/packages/client/src/components/MkChannelPreview.vue
index 8b2e12dc8b..b7462f5504 100644
--- a/packages/client/src/components/MkChannelPreview.vue
+++ b/packages/client/src/components/MkChannelPreview.vue
@@ -52,9 +52,9 @@
 
 <script lang="ts" setup>
 import { computed } from "vue";
+import type { entities } from "firefish-js";
 import { i18n } from "@/i18n";
 import icon from "@/scripts/icon";
-import type { entities } from "firefish-js";
 
 const props = defineProps<{
 	channel: entities.Channel;
diff --git a/packages/client/src/components/MkContainer.vue b/packages/client/src/components/MkContainer.vue
index e933b02dbf..6f7f19dbf0 100644
--- a/packages/client/src/components/MkContainer.vue
+++ b/packages/client/src/components/MkContainer.vue
@@ -1,5 +1,6 @@
 <template>
 	<div
+		ref="el"
 		v-size="{ max: [380] }"
 		class="ukygtjoj _panel"
 		:class="{
@@ -9,7 +10,6 @@
 			scrollable,
 			closed: !showBody,
 		}"
-		ref="el"
 	>
 		<header v-if="showHeader" ref="header">
 			<div class="title"><slot name="header"></slot></div>
diff --git a/packages/client/src/components/MkGalleryPostPreview.vue b/packages/client/src/components/MkGalleryPostPreview.vue
index cbef1c3d75..f4da5d20e8 100644
--- a/packages/client/src/components/MkGalleryPostPreview.vue
+++ b/packages/client/src/components/MkGalleryPostPreview.vue
@@ -33,9 +33,9 @@
 </template>
 
 <script lang="ts" setup>
+import type { entities } from "firefish-js";
 import ImgWithBlurhash from "@/components/MkImgWithBlurhash.vue";
 import { i18n } from "@/i18n";
-import type { entities } from "firefish-js";
 
 defineProps<{
 	post: entities.GalleryPost;
diff --git a/packages/client/src/components/MkInstanceStats.vue b/packages/client/src/components/MkInstanceStats.vue
index 72f987ec84..87b1187e2d 100644
--- a/packages/client/src/components/MkInstanceStats.vue
+++ b/packages/client/src/components/MkInstanceStats.vue
@@ -44,6 +44,7 @@
 <script lang="ts" setup>
 import { onMounted, ref, shallowRef } from "vue";
 import { Chart } from "chart.js";
+import type { entities } from "firefish-js";
 import MkSelect from "@/components/form/select.vue";
 import MkChart from "@/components/MkChart.vue";
 import { useChartTooltip } from "@/scripts/use-chart-tooltip";
@@ -52,7 +53,6 @@ import { i18n } from "@/i18n";
 import MkActiveUsersHeatmap from "@/components/MkActiveUsersHeatmap.vue";
 import MkFolder from "@/components/MkFolder.vue";
 import { initChart } from "@/scripts/init-chart";
-import type { entities } from "firefish-js";
 
 initChart();
 
diff --git a/packages/client/src/components/MkInstanceTicker.vue b/packages/client/src/components/MkInstanceTicker.vue
index bbf999c183..f27cce02a3 100644
--- a/packages/client/src/components/MkInstanceTicker.vue
+++ b/packages/client/src/components/MkInstanceTicker.vue
@@ -17,10 +17,10 @@
 <script lang="ts" setup>
 import { ref } from "vue";
 
+import type { entities } from "firefish-js";
 import { instanceName, version } from "@/config";
 import { instance as Instance } from "@/instance";
 import { getProxiedImageUrlNullable } from "@/scripts/media-proxy";
-import type { entities } from "firefish-js";
 
 const props = defineProps<{
 	instance?: entities.InstanceLite;
diff --git a/packages/client/src/components/MkMediaCaption.vue b/packages/client/src/components/MkMediaCaption.vue
index 32ecceda21..b09024adb7 100644
--- a/packages/client/src/components/MkMediaCaption.vue
+++ b/packages/client/src/components/MkMediaCaption.vue
@@ -69,6 +69,7 @@
 import { computed, onBeforeUnmount, onMounted, ref } from "vue";
 import insertTextAtCursor from "insert-text-at-cursor";
 import { length } from "stringz";
+import type { entities } from "firefish-js";
 import * as os from "@/os";
 import MkModal from "@/components/MkModal.vue";
 import MkButton from "@/components/MkButton.vue";
@@ -76,7 +77,6 @@ import bytes from "@/filters/bytes";
 import number from "@/filters/number";
 import { i18n } from "@/i18n";
 import { instance } from "@/instance";
-import type { entities } from "firefish-js";
 
 const props = withDefaults(
 	defineProps<{
diff --git a/packages/client/src/components/MkNotes.vue b/packages/client/src/components/MkNotes.vue
index d347962768..09db9dcaf0 100644
--- a/packages/client/src/components/MkNotes.vue
+++ b/packages/client/src/components/MkNotes.vue
@@ -40,12 +40,12 @@
 
 <script lang="ts" setup>
 import { ref } from "vue";
+import type { entities } from "firefish-js";
 import type {
 	MkPaginationType,
 	PagingKeyOf,
 	PagingOf,
 } from "@/components/MkPagination.vue";
-import type { entities } from "firefish-js";
 import XNote from "@/components/MkNote.vue";
 import XList from "@/components/MkDateSeparatedList.vue";
 import MkPagination from "@/components/MkPagination.vue";
diff --git a/packages/client/src/components/MkNotification.vue b/packages/client/src/components/MkNotification.vue
index 6dd1a4b721..e3f8259def 100644
--- a/packages/client/src/components/MkNotification.vue
+++ b/packages/client/src/components/MkNotification.vue
@@ -272,6 +272,8 @@
 <script lang="ts" setup>
 import { onMounted, onUnmounted, ref, toRef, watch } from "vue";
 import type { entities } from "firefish-js";
+import type { Connection } from "firefish-js/src/streaming";
+import type { Channels } from "firefish-js/src/streaming.types";
 import XReactionIcon from "@/components/MkReactionIcon.vue";
 import MkFollowButton from "@/components/MkFollowButton.vue";
 import XReactionTooltip from "@/components/MkReactionTooltip.vue";
@@ -285,8 +287,6 @@ import { useTooltip } from "@/scripts/use-tooltip";
 import { defaultStore } from "@/store";
 import { instance } from "@/instance";
 import icon from "@/scripts/icon";
-import type { Connection } from "firefish-js/src/streaming";
-import type { Channels } from "firefish-js/src/streaming.types";
 
 const props = withDefaults(
 	defineProps<{
diff --git a/packages/client/src/components/MkNotificationToast.vue b/packages/client/src/components/MkNotificationToast.vue
index a18a7e3d48..32b0727927 100644
--- a/packages/client/src/components/MkNotificationToast.vue
+++ b/packages/client/src/components/MkNotificationToast.vue
@@ -16,10 +16,10 @@
 
 <script lang="ts" setup>
 import { onMounted, ref } from "vue";
+import type { entities } from "firefish-js";
 import XNotification from "@/components/MkNotification.vue";
 import * as os from "@/os";
 import { defaultStore } from "@/store";
-import type { entities } from "firefish-js";
 
 defineProps<{
 	notification: entities.Notification;
diff --git a/packages/client/src/components/MkPagePreview.vue b/packages/client/src/components/MkPagePreview.vue
index 3a6a45745d..8377770aa0 100644
--- a/packages/client/src/components/MkPagePreview.vue
+++ b/packages/client/src/components/MkPagePreview.vue
@@ -34,9 +34,9 @@
 </template>
 
 <script lang="ts" setup>
+import type { entities } from "firefish-js";
 import { userName } from "@/filters/user";
 import { ui } from "@/config";
-import type { entities } from "firefish-js";
 
 defineProps<{
 	page: entities.Page;
diff --git a/packages/client/src/components/MkReactionTooltip.vue b/packages/client/src/components/MkReactionTooltip.vue
index 1286fc4c73..68a1d6b625 100644
--- a/packages/client/src/components/MkReactionTooltip.vue
+++ b/packages/client/src/components/MkReactionTooltip.vue
@@ -20,9 +20,9 @@
 
 <script lang="ts" setup>
 import type { Ref } from "vue";
+import type { entities } from "firefish-js";
 import MkTooltip from "./MkTooltip.vue";
 import XReactionIcon from "@/components/MkReactionIcon.vue";
-import type { entities } from "firefish-js";
 
 defineProps<{
 	showing: Ref<boolean>;
diff --git a/packages/client/src/components/MkReactionsViewer.details.vue b/packages/client/src/components/MkReactionsViewer.details.vue
index 14c2828d45..7c5f118f98 100644
--- a/packages/client/src/components/MkReactionsViewer.details.vue
+++ b/packages/client/src/components/MkReactionsViewer.details.vue
@@ -3,8 +3,8 @@
 		ref="tooltip"
 		:target-element="targetElement"
 		:max-width="340"
-		@closed="emit('closed')"
 		:showing="showing"
+		@closed="emit('closed')"
 	>
 		<div class="bqxuuuey">
 			<div class="reaction">
@@ -31,9 +31,9 @@
 
 <script lang="ts" setup>
 import type { Ref } from "vue";
+import type { entities } from "firefish-js";
 import MkTooltip from "./MkTooltip.vue";
 import XReactionIcon from "@/components/MkReactionIcon.vue";
-import type { entities } from "firefish-js";
 
 defineProps<{
 	showing: Ref<boolean>;
diff --git a/packages/client/src/components/MkSignin.vue b/packages/client/src/components/MkSignin.vue
index a49662d974..d2c1159642 100644
--- a/packages/client/src/components/MkSignin.vue
+++ b/packages/client/src/components/MkSignin.vue
@@ -136,6 +136,7 @@
 <script lang="ts" setup>
 import { defineAsyncComponent, ref } from "vue";
 import { toUnicode } from "punycode/";
+import type { entities } from "firefish-js";
 import MkButton from "@/components/MkButton.vue";
 import MkInput from "@/components/form/input.vue";
 import MkInfo from "@/components/MkInfo.vue";
@@ -145,7 +146,6 @@ import * as os from "@/os";
 import { signIn } from "@/account";
 import { i18n } from "@/i18n";
 import icon from "@/scripts/icon";
-import type { entities } from "firefish-js";
 
 const signing = ref(false);
 const user = ref<entities.UserDetailed | null>(null);
diff --git a/packages/client/src/components/MkUserList.vue b/packages/client/src/components/MkUserList.vue
index 5043b920ff..a9606c69d7 100644
--- a/packages/client/src/components/MkUserList.vue
+++ b/packages/client/src/components/MkUserList.vue
@@ -26,6 +26,7 @@
 
 <script lang="ts" setup>
 import { ref } from "vue";
+import type { entities } from "firefish-js";
 import MkUserInfo from "@/components/MkUserInfo.vue";
 import type {
 	MkPaginationType,
@@ -34,7 +35,6 @@ import type {
 } from "@/components/MkPagination.vue";
 import MkPagination from "@/components/MkPagination.vue";
 import { i18n } from "@/i18n";
-import type { entities } from "firefish-js";
 
 defineProps<{
 	pagination: PagingOf<entities.UserDetailed>;
diff --git a/packages/client/src/components/MkUsersTooltip.vue b/packages/client/src/components/MkUsersTooltip.vue
index 741194221d..30373c6eb3 100644
--- a/packages/client/src/components/MkUsersTooltip.vue
+++ b/packages/client/src/components/MkUsersTooltip.vue
@@ -3,8 +3,8 @@
 		ref="tooltip"
 		:target-element="targetElement"
 		:max-width="250"
-		@closed="emit('closed')"
 		:showing="showing"
+		@closed="emit('closed')"
 	>
 		<div class="beaffaef">
 			<div v-for="u in users" :key="u.id" class="user">
@@ -20,8 +20,8 @@
 
 <script lang="ts" setup>
 import type { Ref } from "vue";
-import MkTooltip from "./MkTooltip.vue";
 import type { entities } from "firefish-js";
+import MkTooltip from "./MkTooltip.vue";
 
 defineProps<{
 	showing: Ref<boolean>;
diff --git a/packages/client/src/components/MkWaitingDialog.vue b/packages/client/src/components/MkWaitingDialog.vue
index 18cec42a49..f2d7688f36 100644
--- a/packages/client/src/components/MkWaitingDialog.vue
+++ b/packages/client/src/components/MkWaitingDialog.vue
@@ -29,7 +29,8 @@
 </template>
 
 <script lang="ts" setup>
-import { MaybeRef, shallowRef, watch, unref } from "vue";
+import type { MaybeRef } from "vue";
+import { shallowRef, unref, watch } from "vue";
 import MkModal from "@/components/MkModal.vue";
 import iconify from "@/scripts/icon";
 
diff --git a/packages/client/src/os.ts b/packages/client/src/os.ts
index 8fbe8b8041..eb4502bf9f 100644
--- a/packages/client/src/os.ts
+++ b/packages/client/src/os.ts
@@ -1,7 +1,7 @@
 // TODO: なんでもかんでもos.tsに突っ込むのやめたいのでよしなに分割する
 
 import { EventEmitter } from "eventemitter3";
-import { type entities, api as firefishApi, type Endpoints } from "firefish-js";
+import { type Endpoints, type entities, api as firefishApi } from "firefish-js";
 import insertTextAtCursor from "insert-text-at-cursor";
 import type { Component, Ref } from "vue";
 import { defineAsyncComponent, markRaw, ref } from "vue";
@@ -176,12 +176,12 @@ export function promiseDialog<T>(
 
 let popupIdCount = 0;
 
-type PopupType = {
+interface PopupType {
 	id: number;
 	component: Component;
 	props: Record<string, unknown>;
 	events: Record<string, unknown>;
-};
+}
 export const popups = ref<PopupType[]>([]);
 
 const zIndexes = {
diff --git a/packages/client/src/pages/admin/abuses.vue b/packages/client/src/pages/admin/abuses.vue
index 72850ed478..772e3f6f55 100644
--- a/packages/client/src/pages/admin/abuses.vue
+++ b/packages/client/src/pages/admin/abuses.vue
@@ -93,6 +93,7 @@
 <script lang="ts" setup>
 import { computed, ref } from "vue";
 
+import type { entities } from "firefish-js";
 import MkSelect from "@/components/form/select.vue";
 import MkPagination, {
 	type MkPaginationType,
@@ -101,7 +102,6 @@ import XAbuseReport from "@/components/MkAbuseReport.vue";
 import { i18n } from "@/i18n";
 import { definePageMetadata } from "@/scripts/page-metadata";
 import icon from "@/scripts/icon";
-import type { entities } from "firefish-js";
 
 const reports = ref<MkPaginationType<typeof pagination.endpoint> | null>(null);
 
diff --git a/packages/client/src/pages/channel-editor.vue b/packages/client/src/pages/channel-editor.vue
index 3791a98d05..e776de30e3 100644
--- a/packages/client/src/pages/channel-editor.vue
+++ b/packages/client/src/pages/channel-editor.vue
@@ -41,6 +41,7 @@
 
 <script lang="ts" setup>
 import { computed, ref, watch } from "vue";
+import type { entities } from "firefish-js";
 import MkTextarea from "@/components/form/textarea.vue";
 import MkButton from "@/components/MkButton.vue";
 import MkInput from "@/components/form/input.vue";
@@ -50,7 +51,6 @@ import { useRouter } from "@/router";
 import { definePageMetadata } from "@/scripts/page-metadata";
 import { i18n } from "@/i18n";
 import icon from "@/scripts/icon";
-import type { entities } from "firefish-js";
 
 const router = useRouter();
 
diff --git a/packages/client/src/pages/channel.vue b/packages/client/src/pages/channel.vue
index b8ec491ecb..d3ef5f2b8b 100644
--- a/packages/client/src/pages/channel.vue
+++ b/packages/client/src/pages/channel.vue
@@ -96,6 +96,7 @@
 
 <script lang="ts" setup>
 import { computed, ref, watch } from "vue";
+import type { entities } from "firefish-js";
 import XPostForm from "@/components/MkPostForm.vue";
 import XTimeline from "@/components/MkTimeline.vue";
 import XChannelFollowButton from "@/components/MkChannelFollowButton.vue";
@@ -105,7 +106,6 @@ import { me } from "@/me";
 import { i18n } from "@/i18n";
 import { definePageMetadata } from "@/scripts/page-metadata";
 import icon from "@/scripts/icon";
-import type { entities } from "firefish-js";
 
 const router = useRouter();
 
diff --git a/packages/client/src/pages/channels.vue b/packages/client/src/pages/channels.vue
index 5286f909d4..c7789b8787 100644
--- a/packages/client/src/pages/channels.vue
+++ b/packages/client/src/pages/channels.vue
@@ -112,6 +112,7 @@
 import { computed, onMounted, ref, watch } from "vue";
 import { Virtual } from "swiper/modules";
 import { Swiper, SwiperSlide } from "swiper/vue";
+import type { Swiper as SwiperType } from "swiper/types";
 import MkChannelList from "@/components/MkChannelList.vue";
 import MkInput from "@/components/form/input.vue";
 import MkRadios from "@/components/form/radios.vue";
@@ -125,7 +126,6 @@ import { defaultStore } from "@/store";
 import icon from "@/scripts/icon";
 import "swiper/scss";
 import "swiper/scss/virtual";
-import type { Swiper as SwiperType } from "swiper/types";
 
 const router = useRouter();
 
diff --git a/packages/client/src/pages/gallery/post.vue b/packages/client/src/pages/gallery/post.vue
index f8c92c1867..91b6b764c5 100644
--- a/packages/client/src/pages/gallery/post.vue
+++ b/packages/client/src/pages/gallery/post.vue
@@ -150,6 +150,7 @@
 
 <script lang="ts" setup>
 import { computed, ref, watch } from "vue";
+import type { entities } from "firefish-js";
 import MkButton from "@/components/MkButton.vue";
 import * as os from "@/os";
 import MkContainer from "@/components/MkContainer.vue";
@@ -164,7 +165,6 @@ import { shareAvailable } from "@/scripts/share-available";
 import { defaultStore } from "@/store";
 import icon from "@/scripts/icon";
 import { isSignedIn, me } from "@/me";
-import type { entities } from "firefish-js";
 
 const router = useRouter();
 
diff --git a/packages/client/src/pages/my-antennas/index.vue b/packages/client/src/pages/my-antennas/index.vue
index 5b645479a9..ef45ad75f7 100644
--- a/packages/client/src/pages/my-antennas/index.vue
+++ b/packages/client/src/pages/my-antennas/index.vue
@@ -54,7 +54,8 @@
 
 <script lang="ts" setup>
 import { computed, onActivated, onDeactivated, ref } from "vue";
-import MkPagination, { MkPaginationType } from "@/components/MkPagination.vue";
+import type { MkPaginationType } from "@/components/MkPagination.vue";
+import MkPagination from "@/components/MkPagination.vue";
 import MkButton from "@/components/MkButton.vue";
 import MkInfo from "@/components/MkInfo.vue";
 import { i18n } from "@/i18n";
diff --git a/packages/client/src/pages/note-history.vue b/packages/client/src/pages/note-history.vue
index 8bc7f225c8..d0c93899aa 100644
--- a/packages/client/src/pages/note-history.vue
+++ b/packages/client/src/pages/note-history.vue
@@ -34,10 +34,10 @@
 
 <script lang="ts" setup>
 import { computed, onMounted, ref } from "vue";
+import type { entities } from "firefish-js";
 import MkPagination, {
 	type MkPaginationType,
 } from "@/components/MkPagination.vue";
-import type { entities } from "firefish-js";
 import { api } from "@/os";
 import XList from "@/components/MkDateSeparatedList.vue";
 import XNote from "@/components/MkNote.vue";
diff --git a/packages/client/src/scripts/use-chart-tooltip.ts b/packages/client/src/scripts/use-chart-tooltip.ts
index bc6c5b1eeb..62347ede8d 100644
--- a/packages/client/src/scripts/use-chart-tooltip.ts
+++ b/packages/client/src/scripts/use-chart-tooltip.ts
@@ -1,13 +1,13 @@
 import { onDeactivated, onUnmounted, ref } from "vue";
+import type { Color, TooltipOptions } from "chart.js";
 import * as os from "@/os";
 import MkChartTooltip from "@/components/MkChartTooltip.vue";
-import type { Color, TooltipOptions } from "chart.js";
 
-type ToolTipSerie = {
+interface ToolTipSerie {
 	backgroundColor: Color;
 	borderColor: Color;
 	text: string;
-};
+}
 
 export function useChartTooltip(
 	opts: { position: "top" | "middle" } = { position: "top" },
diff --git a/packages/client/src/store.ts b/packages/client/src/store.ts
index 680dba286f..29393123c0 100644
--- a/packages/client/src/store.ts
+++ b/packages/client/src/store.ts
@@ -1,8 +1,8 @@
 import { markRaw, ref } from "vue";
+import type { ApiTypes, entities } from "firefish-js";
 import { isSignedIn } from "./me";
 import { Storage } from "./pizzax";
 import type { NoteVisibility } from "@/types/note";
-import type { entities, ApiTypes } from "firefish-js";
 
 export const postFormActions: {
 	title: string;
diff --git a/packages/client/src/types/form.ts b/packages/client/src/types/form.ts
index 88f90ebbef..c5e169c465 100644
--- a/packages/client/src/types/form.ts
+++ b/packages/client/src/types/form.ts
@@ -1,9 +1,9 @@
-export type BaseFormItem = {
+export interface BaseFormItem {
 	hidden?: boolean;
 	label?: string;
 	description?: string;
 	required?: boolean;
-};
+}
 
 export type FormItemTextInput = BaseFormItem & {
 	type: "string";
diff --git a/packages/client/src/types/note.ts b/packages/client/src/types/note.ts
index 7f4de74d77..17338c02b8 100644
--- a/packages/client/src/types/note.ts
+++ b/packages/client/src/types/note.ts
@@ -2,7 +2,7 @@ import type { noteVisibilities } from "firefish-js";
 
 export type NoteVisibility = (typeof noteVisibilities)[number] | "private";
 
-export type NoteTranslation = {
+export interface NoteTranslation {
 	sourceLang: string;
 	text: string;
-};
+}
diff --git a/packages/client/src/types/post-form.ts b/packages/client/src/types/post-form.ts
index a535ef812f..c8d92f4636 100644
--- a/packages/client/src/types/post-form.ts
+++ b/packages/client/src/types/post-form.ts
@@ -1,11 +1,11 @@
 import type { entities } from "firefish-js";
 
-export type PollType = {
+export interface PollType {
 	choices: string[];
 	multiple: boolean;
 	expiresAt: string | null;
 	expiredAfter: number | null;
-};
+}
 
 export type NoteDraft = entities.Note & {
 	poll?: PollType;

From c6e27762983bd8140adfe4cbbe93dc96c07113ef Mon Sep 17 00:00:00 2001
From: naskya <m@naskya.net>
Date: Fri, 19 Apr 2024 03:42:49 +0900
Subject: [PATCH 085/110] chore (backend): remove a horrible and unused
 function

---
 packages/backend/src/db/postgre.ts | 28 ----------------------------
 1 file changed, 28 deletions(-)

diff --git a/packages/backend/src/db/postgre.ts b/packages/backend/src/db/postgre.ts
index 19f7e7816a..6baccaa271 100644
--- a/packages/backend/src/db/postgre.ts
+++ b/packages/backend/src/db/postgre.ts
@@ -237,31 +237,3 @@ export async function initDb(force = false) {
 		await db.initialize();
 	}
 }
-
-export async function resetDb() {
-	const reset = async () => {
-		await redisClient.flushdb();
-		const tables = await db.query(`SELECT relname AS "table"
-		FROM pg_class C LEFT JOIN pg_namespace N ON (N.oid = C.relnamespace)
-		WHERE nspname NOT IN ('pg_catalog', 'information_schema')
-			AND C.relkind = 'r'
-			AND nspname !~ '^pg_toast';`);
-		for (const table of tables) {
-			await db.query(`DELETE FROM "${table.table}" CASCADE`);
-		}
-	};
-
-	for (let i = 1; i <= 3; i++) {
-		try {
-			await reset();
-		} catch (e) {
-			if (i === 3) {
-				throw e;
-			} else {
-				await new Promise((resolve) => setTimeout(resolve, 1000));
-				continue;
-			}
-		}
-		break;
-	}
-}

From 4823abd3a9415cea56f703854e8ea344a65f644b Mon Sep 17 00:00:00 2001
From: yumeko <yumeko@mainichi.social>
Date: Fri, 19 Apr 2024 03:34:25 +0300
Subject: [PATCH 086/110] Add usageHint field to DriveFile, and fill
 accordingly when operating on Persons

---
 .../migration/1713451569342-AddDriveFileUsage.ts  | 15 +++++++++++++++
 .../backend/src/models/entities/drive-file.ts     |  7 +++++++
 .../src/remote/activitypub/models/image.ts        |  5 ++++-
 .../backend/src/remote/activitypub/models/note.ts |  4 ++--
 .../src/remote/activitypub/models/person.ts       |  8 ++++----
 packages/backend/src/services/drive/add-file.ts   | 10 ++++++++++
 .../backend/src/services/drive/upload-from-url.ts | 11 +++++++++--
 7 files changed, 51 insertions(+), 9 deletions(-)
 create mode 100644 packages/backend/src/migration/1713451569342-AddDriveFileUsage.ts

diff --git a/packages/backend/src/migration/1713451569342-AddDriveFileUsage.ts b/packages/backend/src/migration/1713451569342-AddDriveFileUsage.ts
new file mode 100644
index 0000000000..c0c96fe74c
--- /dev/null
+++ b/packages/backend/src/migration/1713451569342-AddDriveFileUsage.ts
@@ -0,0 +1,15 @@
+import type { MigrationInterface, QueryRunner } from "typeorm";
+
+export class AddDriveFileUsage1713451569342 implements MigrationInterface {
+	public async up(queryRunner: QueryRunner): Promise<void> {
+		await queryRunner.query(
+			`ALTER TABLE "drive_file" ADD "usageHint" character varying(16) DEFAULT NULL`,
+		);
+	}
+
+	public async down(queryRunner: QueryRunner): Promise<void> {
+		await queryRunner.query(
+			`ALTER TABLE "drive_file" DROP COLUMN "usageHint"`
+		);
+	}
+}
diff --git a/packages/backend/src/models/entities/drive-file.ts b/packages/backend/src/models/entities/drive-file.ts
index 3c49e89fd5..b5717d62cd 100644
--- a/packages/backend/src/models/entities/drive-file.ts
+++ b/packages/backend/src/models/entities/drive-file.ts
@@ -177,6 +177,13 @@ export class DriveFile {
 	})
 	public isSensitive: boolean;
 
+	@Column("varchar", {
+		length: 16,
+		nullable: true,
+		comment: "Hint for what the file is used for.",
+	})
+	public usageHint: string | null;
+
 	/**
 	 * 外部の(信頼されていない)URLへの直リンクか否か
 	 */
diff --git a/packages/backend/src/remote/activitypub/models/image.ts b/packages/backend/src/remote/activitypub/models/image.ts
index e2072a963a..23fb720362 100644
--- a/packages/backend/src/remote/activitypub/models/image.ts
+++ b/packages/backend/src/remote/activitypub/models/image.ts
@@ -16,6 +16,7 @@ const logger = apLogger;
 export async function createImage(
 	actor: CacheableRemoteUser,
 	value: any,
+	usage: 'avatar' | 'banner' | null
 ): Promise<DriveFile> {
 	// Skip if author is frozen.
 	if (actor.isSuspended) {
@@ -43,6 +44,7 @@ export async function createImage(
 		sensitive: image.sensitive,
 		isLink: !instance.cacheRemoteFiles,
 		comment: truncate(image.name, DB_MAX_IMAGE_COMMENT_LENGTH),
+		usageHint: usage
 	});
 
 	if (file.isLink) {
@@ -73,9 +75,10 @@ export async function createImage(
 export async function resolveImage(
 	actor: CacheableRemoteUser,
 	value: any,
+	usage: 'avatar' | 'banner' | null,
 ): Promise<DriveFile> {
 	// TODO
 
 	// Fetch from remote server and register
-	return await createImage(actor, value);
+	return await createImage(actor, value, usage);
 }
diff --git a/packages/backend/src/remote/activitypub/models/note.ts b/packages/backend/src/remote/activitypub/models/note.ts
index ad59930457..5e1c3829a7 100644
--- a/packages/backend/src/remote/activitypub/models/note.ts
+++ b/packages/backend/src/remote/activitypub/models/note.ts
@@ -213,7 +213,7 @@ export async function createNote(
 		? (
 				await Promise.all(
 					note.attachment.map(
-						(x) => limit(() => resolveImage(actor, x)) as Promise<DriveFile>,
+						(x) => limit(() => resolveImage(actor, x, null)) as Promise<DriveFile>,
 					),
 				)
 			).filter((image) => image != null)
@@ -616,7 +616,7 @@ export async function updateNote(value: string | IObject, resolver?: Resolver) {
 			fileList.map(
 				(x) =>
 					limit(async () => {
-						const file = await resolveImage(actor, x);
+						const file = await resolveImage(actor, x, null);
 						const update: Partial<DriveFile> = {};
 
 						const altText = truncate(x.name, DB_MAX_IMAGE_COMMENT_LENGTH);
diff --git a/packages/backend/src/remote/activitypub/models/person.ts b/packages/backend/src/remote/activitypub/models/person.ts
index e91280125f..47a1152c36 100644
--- a/packages/backend/src/remote/activitypub/models/person.ts
+++ b/packages/backend/src/remote/activitypub/models/person.ts
@@ -362,10 +362,10 @@ export async function createPerson(
 
 	//#region Fetch avatar and header image
 	const [avatar, banner] = await Promise.all(
-		[person.icon, person.image].map((img) =>
+		[person.icon, person.image].map((img, index) =>
 			img == null
 				? Promise.resolve(null)
-				: resolveImage(user!, img).catch(() => null),
+				: resolveImage(user!, img, index === 0 ? "avatar" : index === 1 ? "banner" : null).catch(() => null),
 		),
 	);
 
@@ -438,10 +438,10 @@ export async function updatePerson(
 
 	// Fetch avatar and header image
 	const [avatar, banner] = await Promise.all(
-		[person.icon, person.image].map((img) =>
+		[person.icon, person.image].map((img, index) =>
 			img == null
 				? Promise.resolve(null)
-				: resolveImage(user, img).catch(() => null),
+				: resolveImage(user, img, index === 0 ? "avatar" : index === 1 ? "banner" : null).catch(() => null),
 		),
 	);
 
diff --git a/packages/backend/src/services/drive/add-file.ts b/packages/backend/src/services/drive/add-file.ts
index 24ad9f8f02..cfb871dd93 100644
--- a/packages/backend/src/services/drive/add-file.ts
+++ b/packages/backend/src/services/drive/add-file.ts
@@ -65,6 +65,7 @@ function urlPathJoin(
  * @param type Content-Type for original
  * @param hash Hash for original
  * @param size Size for original
+ * @param usage Optional usage hint for file (f.e. "avatar")
  */
 async function save(
 	file: DriveFile,
@@ -73,6 +74,7 @@ async function save(
 	type: string,
 	hash: string,
 	size: number,
+	usage: string | null = null
 ): Promise<DriveFile> {
 	// thunbnail, webpublic を必要なら生成
 	const alts = await generateAlts(path, type, !file.uri);
@@ -161,6 +163,7 @@ async function save(
 		file.md5 = hash;
 		file.size = size;
 		file.storedInternal = false;
+		file.usageHint = usage ?? null;
 
 		return await DriveFiles.insert(file).then((x) =>
 			DriveFiles.findOneByOrFail(x.identifiers[0]),
@@ -204,6 +207,7 @@ async function save(
 		file.type = type;
 		file.md5 = hash;
 		file.size = size;
+		file.usageHint = usage ?? null;
 
 		return await DriveFiles.insert(file).then((x) =>
 			DriveFiles.findOneByOrFail(x.identifiers[0]),
@@ -450,6 +454,9 @@ type AddFileArgs = {
 
 	requestIp?: string | null;
 	requestHeaders?: Record<string, string> | null;
+
+	/** Whether this file has a known use case, like user avatar or instance icon */
+	usageHint?: string | null;
 };
 
 /**
@@ -469,6 +476,7 @@ export async function addFile({
 	sensitive = null,
 	requestIp = null,
 	requestHeaders = null,
+	usageHint = null,
 }: AddFileArgs): Promise<DriveFile> {
 	const info = await getFileInfo(path);
 	logger.info(`${JSON.stringify(info)}`);
@@ -581,6 +589,7 @@ export async function addFile({
 	file.isLink = isLink;
 	file.requestIp = requestIp;
 	file.requestHeaders = requestHeaders;
+	file.usageHint = usageHint;
 	file.isSensitive = user
 		? Users.isLocalUser(user) &&
 			(instance!.markLocalFilesNsfwByDefault || profile!.alwaysMarkNsfw)
@@ -639,6 +648,7 @@ export async function addFile({
 			info.type.mime,
 			info.md5,
 			info.size,
+			usageHint
 		);
 	}
 
diff --git a/packages/backend/src/services/drive/upload-from-url.ts b/packages/backend/src/services/drive/upload-from-url.ts
index 551d3757ca..238f8714fa 100644
--- a/packages/backend/src/services/drive/upload-from-url.ts
+++ b/packages/backend/src/services/drive/upload-from-url.ts
@@ -13,7 +13,11 @@ const logger = driveLogger.createSubLogger("downloader");
 
 type Args = {
 	url: string;
-	user: { id: User["id"]; host: User["host"] } | null;
+	user: {
+		id: User["id"];
+		host: User["host"];
+		driveCapacityOverrideMb: User["driveCapacityOverrideMb"];
+	} | null;
 	folderId?: DriveFolder["id"] | null;
 	uri?: string | null;
 	sensitive?: boolean;
@@ -22,6 +26,7 @@ type Args = {
 	comment?: string | null;
 	requestIp?: string | null;
 	requestHeaders?: Record<string, string> | null;
+	usageHint?: string | null;
 };
 
 export async function uploadFromUrl({
@@ -35,6 +40,7 @@ export async function uploadFromUrl({
 	comment = null,
 	requestIp = null,
 	requestHeaders = null,
+	usageHint = null
 }: Args): Promise<DriveFile> {
 	const parsedUrl = new URL(url);
 	if (
@@ -75,9 +81,10 @@ export async function uploadFromUrl({
 			sensitive,
 			requestIp,
 			requestHeaders,
+			usageHint
 		});
 		logger.succ(`Got: ${driveFile.id}`);
-		return driveFile!;
+		return driveFile;
 	} catch (e) {
 		logger.error(`Failed to create drive file:\n${inspect(e)}`);
 		throw e;

From c0f93de94b2183c53ce041b58d028474c0f7ed73 Mon Sep 17 00:00:00 2001
From: yumeko <yumeko@mainichi.social>
Date: Fri, 19 Apr 2024 06:26:40 +0300
Subject: [PATCH 087/110] Set file usage hints on local avatar/banner uploads
 as well + export "valid" values as type

---
 .../backend/src/models/entities/drive-file.ts |  4 +++-
 .../src/remote/activitypub/models/image.ts    |  6 ++---
 .../src/remote/activitypub/models/person.ts   |  4 ++--
 .../src/server/api/endpoints/i/update.ts      | 23 +++++++++++++++++--
 .../backend/src/services/drive/add-file.ts    |  7 +++---
 5 files changed, 33 insertions(+), 11 deletions(-)

diff --git a/packages/backend/src/models/entities/drive-file.ts b/packages/backend/src/models/entities/drive-file.ts
index b5717d62cd..f257280d60 100644
--- a/packages/backend/src/models/entities/drive-file.ts
+++ b/packages/backend/src/models/entities/drive-file.ts
@@ -16,6 +16,8 @@ import { DriveFolder } from "./drive-folder.js";
 import { DB_MAX_IMAGE_COMMENT_LENGTH } from "@/misc/hard-limits.js";
 import { NoteFile } from "./note-file.js";
 
+export type DriveFileUsageHint = "user_avatar" | "user_banner" | null;
+
 @Entity()
 @Index(["userId", "folderId", "id"])
 export class DriveFile {
@@ -182,7 +184,7 @@ export class DriveFile {
 		nullable: true,
 		comment: "Hint for what the file is used for.",
 	})
-	public usageHint: string | null;
+	public usageHint: DriveFileUsageHint;
 
 	/**
 	 * 外部の(信頼されていない)URLへの直リンクか否か
diff --git a/packages/backend/src/remote/activitypub/models/image.ts b/packages/backend/src/remote/activitypub/models/image.ts
index 23fb720362..9183b82ebe 100644
--- a/packages/backend/src/remote/activitypub/models/image.ts
+++ b/packages/backend/src/remote/activitypub/models/image.ts
@@ -3,7 +3,7 @@ import type { CacheableRemoteUser } from "@/models/entities/user.js";
 import Resolver from "../resolver.js";
 import { fetchMeta } from "backend-rs";
 import { apLogger } from "../logger.js";
-import type { DriveFile } from "@/models/entities/drive-file.js";
+import type { DriveFile, DriveFileUsageHint } from "@/models/entities/drive-file.js";
 import { DriveFiles } from "@/models/index.js";
 import { truncate } from "@/misc/truncate.js";
 import { DB_MAX_IMAGE_COMMENT_LENGTH } from "@/misc/hard-limits.js";
@@ -16,7 +16,7 @@ const logger = apLogger;
 export async function createImage(
 	actor: CacheableRemoteUser,
 	value: any,
-	usage: 'avatar' | 'banner' | null
+	usage: DriveFileUsageHint
 ): Promise<DriveFile> {
 	// Skip if author is frozen.
 	if (actor.isSuspended) {
@@ -75,7 +75,7 @@ export async function createImage(
 export async function resolveImage(
 	actor: CacheableRemoteUser,
 	value: any,
-	usage: 'avatar' | 'banner' | null,
+	usage: DriveFileUsageHint,
 ): Promise<DriveFile> {
 	// TODO
 
diff --git a/packages/backend/src/remote/activitypub/models/person.ts b/packages/backend/src/remote/activitypub/models/person.ts
index 47a1152c36..64a4c7f5c5 100644
--- a/packages/backend/src/remote/activitypub/models/person.ts
+++ b/packages/backend/src/remote/activitypub/models/person.ts
@@ -365,7 +365,7 @@ export async function createPerson(
 		[person.icon, person.image].map((img, index) =>
 			img == null
 				? Promise.resolve(null)
-				: resolveImage(user!, img, index === 0 ? "avatar" : index === 1 ? "banner" : null).catch(() => null),
+				: resolveImage(user, img, index === 0 ? "user_avatar" : index === 1 ? "user_banner" : null).catch(() => null)
 		),
 	);
 
@@ -441,7 +441,7 @@ export async function updatePerson(
 		[person.icon, person.image].map((img, index) =>
 			img == null
 				? Promise.resolve(null)
-				: resolveImage(user, img, index === 0 ? "avatar" : index === 1 ? "banner" : null).catch(() => null),
+				: resolveImage(user, img, index === 0 ? "user_avatar" : index === 1 ? "user_banner" : null).catch(() => null),
 		),
 	);
 
diff --git a/packages/backend/src/server/api/endpoints/i/update.ts b/packages/backend/src/server/api/endpoints/i/update.ts
index 4389688a12..9d39903eac 100644
--- a/packages/backend/src/server/api/endpoints/i/update.ts
+++ b/packages/backend/src/server/api/endpoints/i/update.ts
@@ -13,6 +13,7 @@ import { normalizeForSearch } from "@/misc/normalize-for-search.js";
 import { verifyLink } from "@/services/fetch-rel-me.js";
 import { ApiError } from "@/server/api/error.js";
 import define from "@/server/api/define.js";
+import { DriveFile } from "@/models/entities/drive-file";
 
 export const meta = {
 	tags: ["account"],
@@ -241,8 +242,9 @@ export default define(meta, paramDef, async (ps, _user, token) => {
 	if (ps.emailNotificationTypes !== undefined)
 		profileUpdates.emailNotificationTypes = ps.emailNotificationTypes;
 
+	let avatar: DriveFile | null = null
 	if (ps.avatarId) {
-		const avatar = await DriveFiles.findOneBy({ id: ps.avatarId });
+		avatar = await DriveFiles.findOneBy({ id: ps.avatarId });
 
 		if (avatar == null || avatar.userId !== user.id)
 			throw new ApiError(meta.errors.noSuchAvatar);
@@ -250,8 +252,9 @@ export default define(meta, paramDef, async (ps, _user, token) => {
 			throw new ApiError(meta.errors.avatarNotAnImage);
 	}
 
+	let banner: DriveFile | null = null
 	if (ps.bannerId) {
-		const banner = await DriveFiles.findOneBy({ id: ps.bannerId });
+		banner = await DriveFiles.findOneBy({ id: ps.bannerId });
 
 		if (banner == null || banner.userId !== user.id)
 			throw new ApiError(meta.errors.noSuchBanner);
@@ -328,6 +331,22 @@ export default define(meta, paramDef, async (ps, _user, token) => {
 	updateUsertags(user, tags);
 	//#endregion
 
+	// Update old/new avatar usage hints
+	if (avatar)
+	{
+		if (user.avatarId)
+			await DriveFiles.update(user.avatarId, {usageHint: null});
+		await DriveFiles.update(avatar.id, {usageHint: "user_avatar"});
+	}
+
+	// Update old/new banner usage hints
+	if (banner)
+	{
+		if (user.bannerId)
+			await DriveFiles.update(user.bannerId, {usageHint: null});
+		await DriveFiles.update(banner.id, {usageHint: "user_banner"});
+	}
+
 	if (Object.keys(updates).length > 0) await Users.update(user.id, updates);
 	if (Object.keys(profileUpdates).length > 0)
 		await UserProfiles.update(user.id, profileUpdates);
diff --git a/packages/backend/src/services/drive/add-file.ts b/packages/backend/src/services/drive/add-file.ts
index cfb871dd93..79f30a651b 100644
--- a/packages/backend/src/services/drive/add-file.ts
+++ b/packages/backend/src/services/drive/add-file.ts
@@ -16,6 +16,7 @@ import {
 	UserProfiles,
 } from "@/models/index.js";
 import { DriveFile } from "@/models/entities/drive-file.js";
+import type { DriveFileUsageHint } from "@/models/entities/drive-file.js";
 import type { IRemoteUser, User } from "@/models/entities/user.js";
 import { genId } from "backend-rs";
 import { isDuplicateKeyValueError } from "@/misc/is-duplicate-key-value-error.js";
@@ -65,7 +66,7 @@ function urlPathJoin(
  * @param type Content-Type for original
  * @param hash Hash for original
  * @param size Size for original
- * @param usage Optional usage hint for file (f.e. "avatar")
+ * @param usage Optional usage hint for file (f.e. "user_avatar")
  */
 async function save(
 	file: DriveFile,
@@ -74,7 +75,7 @@ async function save(
 	type: string,
 	hash: string,
 	size: number,
-	usage: string | null = null
+	usage: DriveFileUsageHint = null
 ): Promise<DriveFile> {
 	// thunbnail, webpublic を必要なら生成
 	const alts = await generateAlts(path, type, !file.uri);
@@ -456,7 +457,7 @@ type AddFileArgs = {
 	requestHeaders?: Record<string, string> | null;
 
 	/** Whether this file has a known use case, like user avatar or instance icon */
-	usageHint?: string | null;
+	usageHint?: DriveFileUsageHint;
 };
 
 /**

From 4aeb0d95ccf7e4427c60ecd915a48922f465e451 Mon Sep 17 00:00:00 2001
From: yumeko <yumeko@mainichi.social>
Date: Fri, 19 Apr 2024 07:03:09 +0300
Subject: [PATCH 088/110] Add DriveFile usageHint field to rust model as well

---
 packages/backend-rs/src/model/entity/drive_file.rs | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/packages/backend-rs/src/model/entity/drive_file.rs b/packages/backend-rs/src/model/entity/drive_file.rs
index e3e4622a62..e5c3995573 100644
--- a/packages/backend-rs/src/model/entity/drive_file.rs
+++ b/packages/backend-rs/src/model/entity/drive_file.rs
@@ -52,6 +52,8 @@ pub struct Model {
     pub request_headers: Option<Json>,
     #[sea_orm(column_name = "requestIp")]
     pub request_ip: Option<String>,
+    #[sea_orm(column_name = "usageHint")]
+    pub usage_hint: Option<String>,
 }
 
 #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]

From 913de651dbb4413728599a1c7df3a646098c750c Mon Sep 17 00:00:00 2001
From: yumeko <yumeko@mainichi.social>
Date: Fri, 19 Apr 2024 07:25:42 +0300
Subject: [PATCH 089/110] When updating (remote) user avatar/banner, clear
 usageHint for the previous drivefile, if any

---
 packages/backend/src/remote/activitypub/models/person.ts | 5 +++++
 1 file changed, 5 insertions(+)

diff --git a/packages/backend/src/remote/activitypub/models/person.ts b/packages/backend/src/remote/activitypub/models/person.ts
index 64a4c7f5c5..5460a0234b 100644
--- a/packages/backend/src/remote/activitypub/models/person.ts
+++ b/packages/backend/src/remote/activitypub/models/person.ts
@@ -10,6 +10,7 @@ import {
 	Followings,
 	UserProfiles,
 	UserPublickeys,
+	DriveFiles,
 } from "@/models/index.js";
 import type { IRemoteUser, CacheableUser } from "@/models/entities/user.js";
 import { User } from "@/models/entities/user.js";
@@ -561,10 +562,14 @@ export async function updatePerson(
 	} as Partial<User>;
 
 	if (avatar) {
+		if (user?.avatarId)
+			await DriveFiles.update(user.avatarId, {usageHint: null});
 		updates.avatarId = avatar.id;
 	}
 
 	if (banner) {
+		if (user?.bannerId)
+			await DriveFiles.update(user.bannerId, {usageHint: null});
 		updates.bannerId = banner.id;
 	}
 

From 968657d26eff412fd18e8b8cab47718f40a0f5e1 Mon Sep 17 00:00:00 2001
From: yumeko <yumeko@mainichi.social>
Date: Fri, 19 Apr 2024 07:54:11 +0300
Subject: [PATCH 090/110] Run format

---
 .../1713451569342-AddDriveFileUsage.ts         |  4 +---
 .../src/remote/activitypub/models/image.ts     |  9 ++++++---
 .../src/remote/activitypub/models/note.ts      |  3 ++-
 .../src/remote/activitypub/models/person.ts    | 16 ++++++++++++----
 .../src/server/api/endpoints/i/update.ts       | 18 ++++++++----------
 .../backend/src/services/drive/add-file.ts     |  4 ++--
 .../src/services/drive/upload-from-url.ts      |  8 ++++----
 7 files changed, 35 insertions(+), 27 deletions(-)

diff --git a/packages/backend/src/migration/1713451569342-AddDriveFileUsage.ts b/packages/backend/src/migration/1713451569342-AddDriveFileUsage.ts
index c0c96fe74c..57c6a73890 100644
--- a/packages/backend/src/migration/1713451569342-AddDriveFileUsage.ts
+++ b/packages/backend/src/migration/1713451569342-AddDriveFileUsage.ts
@@ -8,8 +8,6 @@ export class AddDriveFileUsage1713451569342 implements MigrationInterface {
 	}
 
 	public async down(queryRunner: QueryRunner): Promise<void> {
-		await queryRunner.query(
-			`ALTER TABLE "drive_file" DROP COLUMN "usageHint"`
-		);
+		await queryRunner.query(`ALTER TABLE "drive_file" DROP COLUMN "usageHint"`);
 	}
 }
diff --git a/packages/backend/src/remote/activitypub/models/image.ts b/packages/backend/src/remote/activitypub/models/image.ts
index 9183b82ebe..a6ac698feb 100644
--- a/packages/backend/src/remote/activitypub/models/image.ts
+++ b/packages/backend/src/remote/activitypub/models/image.ts
@@ -3,7 +3,10 @@ import type { CacheableRemoteUser } from "@/models/entities/user.js";
 import Resolver from "../resolver.js";
 import { fetchMeta } from "backend-rs";
 import { apLogger } from "../logger.js";
-import type { DriveFile, DriveFileUsageHint } from "@/models/entities/drive-file.js";
+import type {
+	DriveFile,
+	DriveFileUsageHint,
+} from "@/models/entities/drive-file.js";
 import { DriveFiles } from "@/models/index.js";
 import { truncate } from "@/misc/truncate.js";
 import { DB_MAX_IMAGE_COMMENT_LENGTH } from "@/misc/hard-limits.js";
@@ -16,7 +19,7 @@ const logger = apLogger;
 export async function createImage(
 	actor: CacheableRemoteUser,
 	value: any,
-	usage: DriveFileUsageHint
+	usage: DriveFileUsageHint,
 ): Promise<DriveFile> {
 	// Skip if author is frozen.
 	if (actor.isSuspended) {
@@ -44,7 +47,7 @@ export async function createImage(
 		sensitive: image.sensitive,
 		isLink: !instance.cacheRemoteFiles,
 		comment: truncate(image.name, DB_MAX_IMAGE_COMMENT_LENGTH),
-		usageHint: usage
+		usageHint: usage,
 	});
 
 	if (file.isLink) {
diff --git a/packages/backend/src/remote/activitypub/models/note.ts b/packages/backend/src/remote/activitypub/models/note.ts
index 5e1c3829a7..b2fd67288c 100644
--- a/packages/backend/src/remote/activitypub/models/note.ts
+++ b/packages/backend/src/remote/activitypub/models/note.ts
@@ -213,7 +213,8 @@ export async function createNote(
 		? (
 				await Promise.all(
 					note.attachment.map(
-						(x) => limit(() => resolveImage(actor, x, null)) as Promise<DriveFile>,
+						(x) =>
+							limit(() => resolveImage(actor, x, null)) as Promise<DriveFile>,
 					),
 				)
 			).filter((image) => image != null)
diff --git a/packages/backend/src/remote/activitypub/models/person.ts b/packages/backend/src/remote/activitypub/models/person.ts
index 5460a0234b..657ac7d553 100644
--- a/packages/backend/src/remote/activitypub/models/person.ts
+++ b/packages/backend/src/remote/activitypub/models/person.ts
@@ -366,7 +366,11 @@ export async function createPerson(
 		[person.icon, person.image].map((img, index) =>
 			img == null
 				? Promise.resolve(null)
-				: resolveImage(user, img, index === 0 ? "user_avatar" : index === 1 ? "user_banner" : null).catch(() => null)
+				: resolveImage(
+						user,
+						img,
+						index === 0 ? "user_avatar" : index === 1 ? "user_banner" : null,
+					).catch(() => null),
 		),
 	);
 
@@ -442,7 +446,11 @@ export async function updatePerson(
 		[person.icon, person.image].map((img, index) =>
 			img == null
 				? Promise.resolve(null)
-				: resolveImage(user, img, index === 0 ? "user_avatar" : index === 1 ? "user_banner" : null).catch(() => null),
+				: resolveImage(
+						user,
+						img,
+						index === 0 ? "user_avatar" : index === 1 ? "user_banner" : null,
+					).catch(() => null),
 		),
 	);
 
@@ -563,13 +571,13 @@ export async function updatePerson(
 
 	if (avatar) {
 		if (user?.avatarId)
-			await DriveFiles.update(user.avatarId, {usageHint: null});
+			await DriveFiles.update(user.avatarId, { usageHint: null });
 		updates.avatarId = avatar.id;
 	}
 
 	if (banner) {
 		if (user?.bannerId)
-			await DriveFiles.update(user.bannerId, {usageHint: null});
+			await DriveFiles.update(user.bannerId, { usageHint: null });
 		updates.bannerId = banner.id;
 	}
 
diff --git a/packages/backend/src/server/api/endpoints/i/update.ts b/packages/backend/src/server/api/endpoints/i/update.ts
index 9d39903eac..29d03cc465 100644
--- a/packages/backend/src/server/api/endpoints/i/update.ts
+++ b/packages/backend/src/server/api/endpoints/i/update.ts
@@ -242,7 +242,7 @@ export default define(meta, paramDef, async (ps, _user, token) => {
 	if (ps.emailNotificationTypes !== undefined)
 		profileUpdates.emailNotificationTypes = ps.emailNotificationTypes;
 
-	let avatar: DriveFile | null = null
+	let avatar: DriveFile | null = null;
 	if (ps.avatarId) {
 		avatar = await DriveFiles.findOneBy({ id: ps.avatarId });
 
@@ -252,7 +252,7 @@ export default define(meta, paramDef, async (ps, _user, token) => {
 			throw new ApiError(meta.errors.avatarNotAnImage);
 	}
 
-	let banner: DriveFile | null = null
+	let banner: DriveFile | null = null;
 	if (ps.bannerId) {
 		banner = await DriveFiles.findOneBy({ id: ps.bannerId });
 
@@ -332,19 +332,17 @@ export default define(meta, paramDef, async (ps, _user, token) => {
 	//#endregion
 
 	// Update old/new avatar usage hints
-	if (avatar)
-	{
+	if (avatar) {
 		if (user.avatarId)
-			await DriveFiles.update(user.avatarId, {usageHint: null});
-		await DriveFiles.update(avatar.id, {usageHint: "user_avatar"});
+			await DriveFiles.update(user.avatarId, { usageHint: null });
+		await DriveFiles.update(avatar.id, { usageHint: "user_avatar" });
 	}
 
 	// Update old/new banner usage hints
-	if (banner)
-	{
+	if (banner) {
 		if (user.bannerId)
-			await DriveFiles.update(user.bannerId, {usageHint: null});
-		await DriveFiles.update(banner.id, {usageHint: "user_banner"});
+			await DriveFiles.update(user.bannerId, { usageHint: null });
+		await DriveFiles.update(banner.id, { usageHint: "user_banner" });
 	}
 
 	if (Object.keys(updates).length > 0) await Users.update(user.id, updates);
diff --git a/packages/backend/src/services/drive/add-file.ts b/packages/backend/src/services/drive/add-file.ts
index 79f30a651b..2f975e1984 100644
--- a/packages/backend/src/services/drive/add-file.ts
+++ b/packages/backend/src/services/drive/add-file.ts
@@ -75,7 +75,7 @@ async function save(
 	type: string,
 	hash: string,
 	size: number,
-	usage: DriveFileUsageHint = null
+	usage: DriveFileUsageHint = null,
 ): Promise<DriveFile> {
 	// thunbnail, webpublic を必要なら生成
 	const alts = await generateAlts(path, type, !file.uri);
@@ -649,7 +649,7 @@ export async function addFile({
 			info.type.mime,
 			info.md5,
 			info.size,
-			usageHint
+			usageHint,
 		);
 	}
 
diff --git a/packages/backend/src/services/drive/upload-from-url.ts b/packages/backend/src/services/drive/upload-from-url.ts
index 238f8714fa..a96e8e3262 100644
--- a/packages/backend/src/services/drive/upload-from-url.ts
+++ b/packages/backend/src/services/drive/upload-from-url.ts
@@ -3,7 +3,7 @@ import type { User } from "@/models/entities/user.js";
 import { createTemp } from "@/misc/create-temp.js";
 import { downloadUrl, isPrivateIp } from "@/misc/download-url.js";
 import type { DriveFolder } from "@/models/entities/drive-folder.js";
-import type { DriveFile } from "@/models/entities/drive-file.js";
+import type { DriveFile, DriveFileUsageHint } from "@/models/entities/drive-file.js";
 import { DriveFiles } from "@/models/index.js";
 import { driveLogger } from "./logger.js";
 import { addFile } from "./add-file.js";
@@ -26,7 +26,7 @@ type Args = {
 	comment?: string | null;
 	requestIp?: string | null;
 	requestHeaders?: Record<string, string> | null;
-	usageHint?: string | null;
+	usageHint?: DriveFileUsageHint;
 };
 
 export async function uploadFromUrl({
@@ -40,7 +40,7 @@ export async function uploadFromUrl({
 	comment = null,
 	requestIp = null,
 	requestHeaders = null,
-	usageHint = null
+	usageHint = null,
 }: Args): Promise<DriveFile> {
 	const parsedUrl = new URL(url);
 	if (
@@ -81,7 +81,7 @@ export async function uploadFromUrl({
 			sensitive,
 			requestIp,
 			requestHeaders,
-			usageHint
+			usageHint,
 		});
 		logger.succ(`Got: ${driveFile.id}`);
 		return driveFile;

From 1be5373dfc839be589668dea84009eb33fa0213b Mon Sep 17 00:00:00 2001
From: naskya <m@naskya.net>
Date: Fri, 19 Apr 2024 21:59:12 +0900
Subject: [PATCH 091/110] chore (backend-rs): make exported enum compatible w/
 TypeScript's string enum

---
 packages/backend-rs/Makefile                  |   2 +-
 packages/backend-rs/index.d.ts                | 132 +++++++++---------
 packages/backend-rs/package.json              |   4 +-
 .../src/model/entity/sea_orm_active_enums.rs  |  20 +--
 4 files changed, 79 insertions(+), 79 deletions(-)

diff --git a/packages/backend-rs/Makefile b/packages/backend-rs/Makefile
index eb4b30c6df..11b614c82a 100644
--- a/packages/backend-rs/Makefile
+++ b/packages/backend-rs/Makefile
@@ -17,7 +17,7 @@ regenerate-entities:
 	  attribute=$$(printf 'cfg_attr(feature = "napi", napi_derive::napi(object, js_name = "%s", use_nullable = true))' "$${jsname}"); \
 	  sed -i "s/NAPI_EXTRA_ATTR_PLACEHOLDER/$${attribute}/" "$${file}"; \
 	done
-	sed -i 's/#\[derive(Debug, Clone, PartialEq, Eq, EnumIter, DeriveActiveEnum)\]/#[derive(Debug, PartialEq, Eq, EnumIter, DeriveActiveEnum)]\n#[cfg_attr(not(feature = "napi"), derive(Clone))]\n#[cfg_attr(feature = "napi", napi_derive::napi)]/' \
+	sed -i 's/#\[derive(Debug, Clone, PartialEq, Eq, EnumIter, DeriveActiveEnum)\]/#[derive(Debug, PartialEq, Eq, EnumIter, DeriveActiveEnum)]\n#[cfg_attr(not(feature = "napi"), derive(Clone))]\n#[cfg_attr(feature = "napi", napi_derive::napi(string_enum = "camelCase"))]/' \
 	  src/model/entity/sea_orm_active_enums.rs
 	cargo fmt --all --
 
diff --git a/packages/backend-rs/index.d.ts b/packages/backend-rs/index.d.ts
index 5ee69969c6..1cf961bd30 100644
--- a/packages/backend-rs/index.d.ts
+++ b/packages/backend-rs/index.d.ts
@@ -772,81 +772,81 @@ export interface ReplyMuting {
   muteeId: string
   muterId: string
 }
-export const enum AntennaSrcEnum {
-  All = 0,
-  Group = 1,
-  Home = 2,
-  Instances = 3,
-  List = 4,
-  Users = 5
+export enum AntennaSrcEnum {
+  All = 'all',
+  Group = 'group',
+  Home = 'home',
+  Instances = 'instances',
+  List = 'list',
+  Users = 'users'
 }
-export const enum MutedNoteReasonEnum {
-  Manual = 0,
-  Other = 1,
-  Spam = 2,
-  Word = 3
+export enum MutedNoteReasonEnum {
+  Manual = 'manual',
+  Other = 'other',
+  Spam = 'spam',
+  Word = 'word'
 }
-export const enum NoteVisibilityEnum {
-  Followers = 0,
-  Hidden = 1,
-  Home = 2,
-  Public = 3,
-  Specified = 4
+export enum NoteVisibilityEnum {
+  Followers = 'followers',
+  Hidden = 'hidden',
+  Home = 'home',
+  Public = 'public',
+  Specified = 'specified'
 }
-export const enum NotificationTypeEnum {
-  App = 0,
-  Follow = 1,
-  FollowRequestAccepted = 2,
-  GroupInvited = 3,
-  Mention = 4,
-  PollEnded = 5,
-  PollVote = 6,
-  Quote = 7,
-  Reaction = 8,
-  ReceiveFollowRequest = 9,
-  Renote = 10,
-  Reply = 11
+export enum NotificationTypeEnum {
+  App = 'app',
+  Follow = 'follow',
+  FollowRequestAccepted = 'followRequestAccepted',
+  GroupInvited = 'groupInvited',
+  Mention = 'mention',
+  PollEnded = 'pollEnded',
+  PollVote = 'pollVote',
+  Quote = 'quote',
+  Reaction = 'reaction',
+  ReceiveFollowRequest = 'receiveFollowRequest',
+  Renote = 'renote',
+  Reply = 'reply'
 }
-export const enum PageVisibilityEnum {
-  Followers = 0,
-  Public = 1,
-  Specified = 2
+export enum PageVisibilityEnum {
+  Followers = 'followers',
+  Public = 'public',
+  Specified = 'specified'
 }
-export const enum PollNotevisibilityEnum {
-  Followers = 0,
-  Home = 1,
-  Public = 2,
-  Specified = 3
+export enum PollNotevisibilityEnum {
+  Followers = 'followers',
+  Home = 'home',
+  Public = 'public',
+  Specified = 'specified'
 }
-export const enum RelayStatusEnum {
-  Accepted = 0,
-  Rejected = 1,
-  Requesting = 2
+export enum RelayStatusEnum {
+  Accepted = 'accepted',
+  Rejected = 'rejected',
+  Requesting = 'requesting'
 }
-export const enum UserEmojimodpermEnum {
-  Add = 0,
-  Full = 1,
-  Mod = 2,
-  Unauthorized = 3
+export enum UserEmojimodpermEnum {
+  Add = 'add',
+  Full = 'full',
+  Mod = 'mod',
+  Unauthorized = 'unauthorized'
 }
-export const enum UserProfileFfvisibilityEnum {
-  Followers = 0,
-  Private = 1,
-  Public = 2
+export enum UserProfileFfvisibilityEnum {
+  Followers = 'followers',
+  Private = 'private',
+  Public = 'public'
 }
-export const enum UserProfileMutingnotificationtypesEnum {
-  App = 0,
-  Follow = 1,
-  FollowRequestAccepted = 2,
-  GroupInvited = 3,
-  Mention = 4,
-  PollEnded = 5,
-  PollVote = 6,
-  Quote = 7,
-  Reaction = 8,
-  ReceiveFollowRequest = 9,
-  Renote = 10,
-  Reply = 11
+export enum UserProfileMutingnotificationtypesEnum {
+  App = 'app',
+  Follow = 'follow',
+  FollowRequestAccepted = 'followRequestAccepted',
+  GroupInvited = 'groupInvited',
+  Mention = 'mention',
+  PollEnded = 'pollEnded',
+  PollVote = 'pollVote',
+  Quote = 'quote',
+  Reaction = 'reaction',
+  ReceiveFollowRequest = 'receiveFollowRequest',
+  Renote = 'renote',
+  Reply = 'reply'
 }
 export interface Signin {
   id: string
diff --git a/packages/backend-rs/package.json b/packages/backend-rs/package.json
index 69ae09fa49..1f3f49e9fb 100644
--- a/packages/backend-rs/package.json
+++ b/packages/backend-rs/package.json
@@ -33,8 +33,8 @@
 	},
 	"scripts": {
 		"artifacts": "napi artifacts",
-		"build": "napi build --features napi --platform --release ./built/",
-		"build:debug": "napi build --features napi --platform ./built/",
+		"build": "napi build --features napi --no-const-enum --platform --release ./built/",
+		"build:debug": "napi build --features napi --no-const-enum --platform ./built/",
 		"prepublishOnly": "napi prepublish -t npm",
 		"test": "pnpm run cargo:test && pnpm run build:debug && ava",
 		"universal": "napi universal",
diff --git a/packages/backend-rs/src/model/entity/sea_orm_active_enums.rs b/packages/backend-rs/src/model/entity/sea_orm_active_enums.rs
index 861b3a18d0..38820e1bd8 100644
--- a/packages/backend-rs/src/model/entity/sea_orm_active_enums.rs
+++ b/packages/backend-rs/src/model/entity/sea_orm_active_enums.rs
@@ -4,7 +4,7 @@ use sea_orm::entity::prelude::*;
 
 #[derive(Debug, PartialEq, Eq, EnumIter, DeriveActiveEnum)]
 #[cfg_attr(not(feature = "napi"), derive(Clone))]
-#[cfg_attr(feature = "napi", napi_derive::napi)]
+#[cfg_attr(feature = "napi", napi_derive::napi(string_enum = "camelCase"))]
 #[sea_orm(rs_type = "String", db_type = "Enum", enum_name = "antenna_src_enum")]
 pub enum AntennaSrcEnum {
     #[sea_orm(string_value = "all")]
@@ -22,7 +22,7 @@ pub enum AntennaSrcEnum {
 }
 #[derive(Debug, PartialEq, Eq, EnumIter, DeriveActiveEnum)]
 #[cfg_attr(not(feature = "napi"), derive(Clone))]
-#[cfg_attr(feature = "napi", napi_derive::napi)]
+#[cfg_attr(feature = "napi", napi_derive::napi(string_enum = "camelCase"))]
 #[sea_orm(
     rs_type = "String",
     db_type = "Enum",
@@ -40,7 +40,7 @@ pub enum MutedNoteReasonEnum {
 }
 #[derive(Debug, PartialEq, Eq, EnumIter, DeriveActiveEnum)]
 #[cfg_attr(not(feature = "napi"), derive(Clone))]
-#[cfg_attr(feature = "napi", napi_derive::napi)]
+#[cfg_attr(feature = "napi", napi_derive::napi(string_enum = "camelCase"))]
 #[sea_orm(
     rs_type = "String",
     db_type = "Enum",
@@ -60,7 +60,7 @@ pub enum NoteVisibilityEnum {
 }
 #[derive(Debug, PartialEq, Eq, EnumIter, DeriveActiveEnum)]
 #[cfg_attr(not(feature = "napi"), derive(Clone))]
-#[cfg_attr(feature = "napi", napi_derive::napi)]
+#[cfg_attr(feature = "napi", napi_derive::napi(string_enum = "camelCase"))]
 #[sea_orm(
     rs_type = "String",
     db_type = "Enum",
@@ -94,7 +94,7 @@ pub enum NotificationTypeEnum {
 }
 #[derive(Debug, PartialEq, Eq, EnumIter, DeriveActiveEnum)]
 #[cfg_attr(not(feature = "napi"), derive(Clone))]
-#[cfg_attr(feature = "napi", napi_derive::napi)]
+#[cfg_attr(feature = "napi", napi_derive::napi(string_enum = "camelCase"))]
 #[sea_orm(
     rs_type = "String",
     db_type = "Enum",
@@ -110,7 +110,7 @@ pub enum PageVisibilityEnum {
 }
 #[derive(Debug, PartialEq, Eq, EnumIter, DeriveActiveEnum)]
 #[cfg_attr(not(feature = "napi"), derive(Clone))]
-#[cfg_attr(feature = "napi", napi_derive::napi)]
+#[cfg_attr(feature = "napi", napi_derive::napi(string_enum = "camelCase"))]
 #[sea_orm(
     rs_type = "String",
     db_type = "Enum",
@@ -128,7 +128,7 @@ pub enum PollNotevisibilityEnum {
 }
 #[derive(Debug, PartialEq, Eq, EnumIter, DeriveActiveEnum)]
 #[cfg_attr(not(feature = "napi"), derive(Clone))]
-#[cfg_attr(feature = "napi", napi_derive::napi)]
+#[cfg_attr(feature = "napi", napi_derive::napi(string_enum = "camelCase"))]
 #[sea_orm(rs_type = "String", db_type = "Enum", enum_name = "relay_status_enum")]
 pub enum RelayStatusEnum {
     #[sea_orm(string_value = "accepted")]
@@ -140,7 +140,7 @@ pub enum RelayStatusEnum {
 }
 #[derive(Debug, PartialEq, Eq, EnumIter, DeriveActiveEnum)]
 #[cfg_attr(not(feature = "napi"), derive(Clone))]
-#[cfg_attr(feature = "napi", napi_derive::napi)]
+#[cfg_attr(feature = "napi", napi_derive::napi(string_enum = "camelCase"))]
 #[sea_orm(
     rs_type = "String",
     db_type = "Enum",
@@ -158,7 +158,7 @@ pub enum UserEmojimodpermEnum {
 }
 #[derive(Debug, PartialEq, Eq, EnumIter, DeriveActiveEnum)]
 #[cfg_attr(not(feature = "napi"), derive(Clone))]
-#[cfg_attr(feature = "napi", napi_derive::napi)]
+#[cfg_attr(feature = "napi", napi_derive::napi(string_enum = "camelCase"))]
 #[sea_orm(
     rs_type = "String",
     db_type = "Enum",
@@ -174,7 +174,7 @@ pub enum UserProfileFfvisibilityEnum {
 }
 #[derive(Debug, PartialEq, Eq, EnumIter, DeriveActiveEnum)]
 #[cfg_attr(not(feature = "napi"), derive(Clone))]
-#[cfg_attr(feature = "napi", napi_derive::napi)]
+#[cfg_attr(feature = "napi", napi_derive::napi(string_enum = "camelCase"))]
 #[sea_orm(
     rs_type = "String",
     db_type = "Enum",

From 6c46bb56fd2c13672ac6338b970f090f1f3c0672 Mon Sep 17 00:00:00 2001
From: yumeko <yumeko@mainichi.social>
Date: Fri, 19 Apr 2024 18:22:02 +0300
Subject: [PATCH 092/110] Switch DriveFile's usageHint field to an enum type

---
 .../src/migration/1713451569342-AddDriveFileUsage.ts       | 6 +++++-
 packages/backend/src/models/entities/drive-file.ts         | 7 ++++---
 packages/backend/src/remote/activitypub/models/person.ts   | 4 ++--
 packages/backend/src/server/api/endpoints/i/update.ts      | 4 ++--
 packages/backend/src/services/drive/add-file.ts            | 2 +-
 5 files changed, 14 insertions(+), 9 deletions(-)

diff --git a/packages/backend/src/migration/1713451569342-AddDriveFileUsage.ts b/packages/backend/src/migration/1713451569342-AddDriveFileUsage.ts
index 57c6a73890..3bdb1aafc8 100644
--- a/packages/backend/src/migration/1713451569342-AddDriveFileUsage.ts
+++ b/packages/backend/src/migration/1713451569342-AddDriveFileUsage.ts
@@ -3,11 +3,15 @@ import type { MigrationInterface, QueryRunner } from "typeorm";
 export class AddDriveFileUsage1713451569342 implements MigrationInterface {
 	public async up(queryRunner: QueryRunner): Promise<void> {
 		await queryRunner.query(
-			`ALTER TABLE "drive_file" ADD "usageHint" character varying(16) DEFAULT NULL`,
+			`CREATE TYPE drive_file_usage_hint_enum AS ENUM ('userAvatar', 'userBanner')`,
+		);
+		await queryRunner.query(
+			`ALTER TABLE "drive_file" ADD "usageHint" drive_file_usage_hint_enum DEFAULT NULL`,
 		);
 	}
 
 	public async down(queryRunner: QueryRunner): Promise<void> {
 		await queryRunner.query(`ALTER TABLE "drive_file" DROP COLUMN "usageHint"`);
+		await queryRunner.query(`DROP TYPE drive_file_usage_hint_enum`);
 	}
 }
diff --git a/packages/backend/src/models/entities/drive-file.ts b/packages/backend/src/models/entities/drive-file.ts
index f257280d60..2c6c1bf598 100644
--- a/packages/backend/src/models/entities/drive-file.ts
+++ b/packages/backend/src/models/entities/drive-file.ts
@@ -16,7 +16,7 @@ import { DriveFolder } from "./drive-folder.js";
 import { DB_MAX_IMAGE_COMMENT_LENGTH } from "@/misc/hard-limits.js";
 import { NoteFile } from "./note-file.js";
 
-export type DriveFileUsageHint = "user_avatar" | "user_banner" | null;
+export type DriveFileUsageHint = "userAvatar" | "userBanner" | null;
 
 @Entity()
 @Index(["userId", "folderId", "id"])
@@ -179,8 +179,9 @@ export class DriveFile {
 	})
 	public isSensitive: boolean;
 
-	@Column("varchar", {
-		length: 16,
+	@Column({
+		type: "enum",
+		enum: ["userAvatar", "userBanner"],
 		nullable: true,
 		comment: "Hint for what the file is used for.",
 	})
diff --git a/packages/backend/src/remote/activitypub/models/person.ts b/packages/backend/src/remote/activitypub/models/person.ts
index 657ac7d553..4baa2c021b 100644
--- a/packages/backend/src/remote/activitypub/models/person.ts
+++ b/packages/backend/src/remote/activitypub/models/person.ts
@@ -369,7 +369,7 @@ export async function createPerson(
 				: resolveImage(
 						user,
 						img,
-						index === 0 ? "user_avatar" : index === 1 ? "user_banner" : null,
+						index === 0 ? "userAvatar" : index === 1 ? "userBanner" : null,
 					).catch(() => null),
 		),
 	);
@@ -449,7 +449,7 @@ export async function updatePerson(
 				: resolveImage(
 						user,
 						img,
-						index === 0 ? "user_avatar" : index === 1 ? "user_banner" : null,
+						index === 0 ? "userAvatar" : index === 1 ? "userBanner" : null,
 					).catch(() => null),
 		),
 	);
diff --git a/packages/backend/src/server/api/endpoints/i/update.ts b/packages/backend/src/server/api/endpoints/i/update.ts
index 29d03cc465..4f65c59a9e 100644
--- a/packages/backend/src/server/api/endpoints/i/update.ts
+++ b/packages/backend/src/server/api/endpoints/i/update.ts
@@ -335,14 +335,14 @@ export default define(meta, paramDef, async (ps, _user, token) => {
 	if (avatar) {
 		if (user.avatarId)
 			await DriveFiles.update(user.avatarId, { usageHint: null });
-		await DriveFiles.update(avatar.id, { usageHint: "user_avatar" });
+		await DriveFiles.update(avatar.id, { usageHint: "userAvatar" });
 	}
 
 	// Update old/new banner usage hints
 	if (banner) {
 		if (user.bannerId)
 			await DriveFiles.update(user.bannerId, { usageHint: null });
-		await DriveFiles.update(banner.id, { usageHint: "user_banner" });
+		await DriveFiles.update(banner.id, { usageHint: "userBanner" });
 	}
 
 	if (Object.keys(updates).length > 0) await Users.update(user.id, updates);
diff --git a/packages/backend/src/services/drive/add-file.ts b/packages/backend/src/services/drive/add-file.ts
index 2f975e1984..d180bbabf3 100644
--- a/packages/backend/src/services/drive/add-file.ts
+++ b/packages/backend/src/services/drive/add-file.ts
@@ -66,7 +66,7 @@ function urlPathJoin(
  * @param type Content-Type for original
  * @param hash Hash for original
  * @param size Size for original
- * @param usage Optional usage hint for file (f.e. "user_avatar")
+ * @param usage Optional usage hint for file (f.e. "userAvatar")
  */
 async function save(
 	file: DriveFile,

From ab221c98a74ee665784b773bb6ef02061a61946e Mon Sep 17 00:00:00 2001
From: Lhcfl <Lhcfl@outlook.com>
Date: Sat, 20 Apr 2024 00:05:37 +0800
Subject: [PATCH 093/110] revert unnecessary MaybeRef in components

---
 packages/client/src/components/MkChartTooltip.vue   |  3 +--
 .../client/src/components/MkReactionTooltip.vue     |  3 +--
 .../src/components/MkReactionsViewer.details.vue    |  3 +--
 packages/client/src/components/MkTooltip.vue        | 13 +++----------
 packages/client/src/components/MkUsersTooltip.vue   |  3 +--
 packages/client/src/components/MkWaitingDialog.vue  |  9 ++++-----
 packages/client/src/os.ts                           |  9 +++++++--
 7 files changed, 18 insertions(+), 25 deletions(-)

diff --git a/packages/client/src/components/MkChartTooltip.vue b/packages/client/src/components/MkChartTooltip.vue
index 678a3ccdaa..cafc40413b 100644
--- a/packages/client/src/components/MkChartTooltip.vue
+++ b/packages/client/src/components/MkChartTooltip.vue
@@ -28,11 +28,10 @@
 </template>
 
 <script lang="ts" setup>
-import type { Ref } from "vue";
 import MkTooltip from "./MkTooltip.vue";
 
 const props = defineProps<{
-	showing: Ref<boolean>;
+	showing: boolean;
 	x: number;
 	y: number;
 	title?: string;
diff --git a/packages/client/src/components/MkReactionTooltip.vue b/packages/client/src/components/MkReactionTooltip.vue
index 68a1d6b625..40e8fefade 100644
--- a/packages/client/src/components/MkReactionTooltip.vue
+++ b/packages/client/src/components/MkReactionTooltip.vue
@@ -19,13 +19,12 @@
 </template>
 
 <script lang="ts" setup>
-import type { Ref } from "vue";
 import type { entities } from "firefish-js";
 import MkTooltip from "./MkTooltip.vue";
 import XReactionIcon from "@/components/MkReactionIcon.vue";
 
 defineProps<{
-	showing: Ref<boolean>;
+	showing: boolean;
 	reaction: string;
 	emojis: entities.EmojiLite[];
 	targetElement: HTMLElement;
diff --git a/packages/client/src/components/MkReactionsViewer.details.vue b/packages/client/src/components/MkReactionsViewer.details.vue
index 7c5f118f98..13f8fd8311 100644
--- a/packages/client/src/components/MkReactionsViewer.details.vue
+++ b/packages/client/src/components/MkReactionsViewer.details.vue
@@ -30,13 +30,12 @@
 </template>
 
 <script lang="ts" setup>
-import type { Ref } from "vue";
 import type { entities } from "firefish-js";
 import MkTooltip from "./MkTooltip.vue";
 import XReactionIcon from "@/components/MkReactionIcon.vue";
 
 defineProps<{
-	showing: Ref<boolean>;
+	showing: boolean;
 	reaction: string;
 	users: entities.User[]; // TODO
 	count: number;
diff --git a/packages/client/src/components/MkTooltip.vue b/packages/client/src/components/MkTooltip.vue
index 2ed3c1974b..becc18fe09 100644
--- a/packages/client/src/components/MkTooltip.vue
+++ b/packages/client/src/components/MkTooltip.vue
@@ -5,7 +5,7 @@
 		@after-leave="emit('closed')"
 	>
 		<div
-			v-show="unref(showing)"
+			v-show="showing"
 			ref="el"
 			class="buebdbiu _acrylic _shadow"
 			:style="{ zIndex, maxWidth: maxWidth + 'px' }"
@@ -19,21 +19,14 @@
 </template>
 
 <script lang="ts" setup>
-import {
-	type MaybeRef,
-	nextTick,
-	onMounted,
-	onUnmounted,
-	ref,
-	unref,
-} from "vue";
+import { nextTick, onMounted, onUnmounted, ref } from "vue";
 import * as os from "@/os";
 import { calcPopupPosition } from "@/scripts/popup-position";
 import { defaultStore } from "@/store";
 
 const props = withDefaults(
 	defineProps<{
-		showing: MaybeRef<boolean>;
+		showing: boolean;
 		targetElement?: HTMLElement | null;
 		x?: number;
 		y?: number;
diff --git a/packages/client/src/components/MkUsersTooltip.vue b/packages/client/src/components/MkUsersTooltip.vue
index 30373c6eb3..30213204d6 100644
--- a/packages/client/src/components/MkUsersTooltip.vue
+++ b/packages/client/src/components/MkUsersTooltip.vue
@@ -19,12 +19,11 @@
 </template>
 
 <script lang="ts" setup>
-import type { Ref } from "vue";
 import type { entities } from "firefish-js";
 import MkTooltip from "./MkTooltip.vue";
 
 defineProps<{
-	showing: Ref<boolean>;
+	showing: boolean;
 	users: entities.User[];
 	count: number;
 	targetElement?: HTMLElement;
diff --git a/packages/client/src/components/MkWaitingDialog.vue b/packages/client/src/components/MkWaitingDialog.vue
index f2d7688f36..c35023adcb 100644
--- a/packages/client/src/components/MkWaitingDialog.vue
+++ b/packages/client/src/components/MkWaitingDialog.vue
@@ -13,7 +13,7 @@
 			]"
 		>
 			<i
-				v-if="unref(success)"
+				v-if="success"
 				:class="[$style.icon, $style.success, iconify('ph-check')]"
 			></i>
 			<MkLoading
@@ -29,16 +29,15 @@
 </template>
 
 <script lang="ts" setup>
-import type { MaybeRef } from "vue";
-import { shallowRef, unref, watch } from "vue";
+import { shallowRef, watch } from "vue";
 import MkModal from "@/components/MkModal.vue";
 import iconify from "@/scripts/icon";
 
 const modal = shallowRef<InstanceType<typeof MkModal>>();
 
 const props = defineProps<{
-	success: MaybeRef<boolean>;
-	showing: MaybeRef<boolean>;
+	success: boolean;
+	showing: boolean;
 	text?: string;
 }>();
 
diff --git a/packages/client/src/os.ts b/packages/client/src/os.ts
index eb4502bf9f..c7c62852de 100644
--- a/packages/client/src/os.ts
+++ b/packages/client/src/os.ts
@@ -3,7 +3,7 @@
 import { EventEmitter } from "eventemitter3";
 import { type Endpoints, type entities, api as firefishApi } from "firefish-js";
 import insertTextAtCursor from "insert-text-at-cursor";
-import type { Component, Ref } from "vue";
+import type { Component, MaybeRef, Ref } from "vue";
 import { defineAsyncComponent, markRaw, ref } from "vue";
 import { i18n } from "./i18n";
 import MkDialog from "@/components/MkDialog.vue";
@@ -213,9 +213,13 @@ interface VueComponentConstructor<P, E> {
 
 type NonArrayAble<A> = A extends Array<unknown> ? never : A;
 
+type CanUseRef<T> = {
+	[K in keyof T]: MaybeRef<T[K]>;
+};
+
 export async function popup<Props, Emits>(
 	component: VueComponentConstructor<Props, Emits>,
-	props: Props,
+	props: CanUseRef<Props>,
 	events: Partial<NonArrayAble<NonNullable<Emits>>> = {},
 	disposeEvent?: keyof Partial<NonArrayAble<NonNullable<Emits>>>,
 ) {
@@ -240,6 +244,7 @@ export async function popup<Props, Emits>(
 		id,
 	};
 
+	// Hint: Vue will automatically resolve ref here, so it is safe to use ref in props
 	popups.value.push(state);
 
 	return {

From 781c98dda77e2da84a4f9f7e37aeac8f477464d1 Mon Sep 17 00:00:00 2001
From: Lhcfl <Lhcfl@outlook.com>
Date: Sat, 20 Apr 2024 00:18:36 +0800
Subject: [PATCH 094/110] revert unnecessary `.value` for MkLink

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

diff --git a/packages/client/src/components/MkLink.vue b/packages/client/src/components/MkLink.vue
index 34db74397e..4add7c32da 100644
--- a/packages/client/src/components/MkLink.vue
+++ b/packages/client/src/components/MkLink.vue
@@ -42,7 +42,7 @@ useTooltip(el, (showing) => {
 	os.popup(
 		defineAsyncComponent(() => import("@/components/MkUrlPreviewPopup.vue")),
 		{
-			showing: showing.value,
+			showing,
 			url: props.url,
 			source: el.value,
 		},

From 207855b0e8da93e681c75aa7909d12b78e2e2200 Mon Sep 17 00:00:00 2001
From: Lhcfl <Lhcfl@outlook.com>
Date: Sat, 20 Apr 2024 01:03:22 +0800
Subject: [PATCH 095/110] fix: use settings from reactionPicker for
 non-reaction emoji picker

---
 packages/client/src/components/MkEmojiPicker.vue     | 12 +++---------
 .../client/src/components/MkEmojiPickerDialog.vue    |  2 +-
 packages/client/src/components/MkPostForm.vue        |  2 +-
 packages/client/src/scripts/reaction-picker.ts       |  4 ++--
 4 files changed, 7 insertions(+), 13 deletions(-)

diff --git a/packages/client/src/components/MkEmojiPicker.vue b/packages/client/src/components/MkEmojiPicker.vue
index afc2f15817..c731fe9b0e 100644
--- a/packages/client/src/components/MkEmojiPicker.vue
+++ b/packages/client/src/components/MkEmojiPicker.vue
@@ -231,15 +231,9 @@ const unicodeEmojiSkinToneLabels = [
 	i18n.ts._skinTones?.dark ?? "Dark",
 ];
 
-const size = computed(() =>
-	props.asReactionPicker ? reactionPickerSize.value : 1,
-);
-const width = computed(() =>
-	props.asReactionPicker ? reactionPickerWidth.value : 3,
-);
-const height = computed(() =>
-	props.asReactionPicker ? reactionPickerHeight.value : 2,
-);
+const size = reactionPickerSize;
+const width = reactionPickerWidth;
+const height = reactionPickerHeight;
 const customEmojiCategories = emojiCategories;
 const customEmojis = instance.emojis;
 const q = ref<string | null>(null);
diff --git a/packages/client/src/components/MkEmojiPickerDialog.vue b/packages/client/src/components/MkEmojiPickerDialog.vue
index 426bbf9469..decf49fd38 100644
--- a/packages/client/src/components/MkEmojiPickerDialog.vue
+++ b/packages/client/src/components/MkEmojiPickerDialog.vue
@@ -39,7 +39,7 @@ import { defaultStore } from "@/store";
 withDefaults(
 	defineProps<{
 		manualShowing?: boolean | null;
-		src?: HTMLElement;
+		src?: HTMLElement | null;
 		showPinned?: boolean;
 		asReactionPicker?: boolean;
 	}>(),
diff --git a/packages/client/src/components/MkPostForm.vue b/packages/client/src/components/MkPostForm.vue
index 0a03dbb30e..582da6c6d4 100644
--- a/packages/client/src/components/MkPostForm.vue
+++ b/packages/client/src/components/MkPostForm.vue
@@ -1188,7 +1188,7 @@ async function insertEmoji(ev: MouseEvent) {
 	os.openEmojiPicker(
 		(ev.currentTarget ?? ev.target) as HTMLElement,
 		{},
-		textareaEl.value,
+		textareaEl.value!,
 	);
 }
 
diff --git a/packages/client/src/scripts/reaction-picker.ts b/packages/client/src/scripts/reaction-picker.ts
index 353a032d32..207d582343 100644
--- a/packages/client/src/scripts/reaction-picker.ts
+++ b/packages/client/src/scripts/reaction-picker.ts
@@ -24,14 +24,14 @@ class ReactionPicker {
 			},
 			{
 				done: (reaction) => {
-					this.onChosen!(reaction);
+					this.onChosen?.(reaction);
 				},
 				close: () => {
 					this.manualShowing.value = false;
 				},
 				closed: () => {
 					this.src.value = null;
-					this.onClosed!();
+					this.onClosed?.();
 				},
 			},
 		);

From 5c4a773ecf1932ad5aefd50f50f824dcea1c9505 Mon Sep 17 00:00:00 2001
From: naskya <m@naskya.net>
Date: Sat, 20 Apr 2024 03:09:18 +0900
Subject: [PATCH 096/110] chore (backend): qualify Node.js builtin modules

---
 packages/backend/src/server/api/streaming.ts | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/packages/backend/src/server/api/streaming.ts b/packages/backend/src/server/api/streaming.ts
index a25984ec3e..12f97d8018 100644
--- a/packages/backend/src/server/api/streaming.ts
+++ b/packages/backend/src/server/api/streaming.ts
@@ -1,6 +1,6 @@
 import type * as http from "node:http";
-import { EventEmitter } from "events";
-import type { ParsedUrlQuery } from "querystring";
+import { EventEmitter } from "node:events";
+import type { ParsedUrlQuery } from "node:querystring";
 import * as websocket from "websocket";
 
 import { subscriber as redisClient } from "@/db/redis.js";

From a2699e66871a237e124f03b00c8d8e89e75cdbce Mon Sep 17 00:00:00 2001
From: naskya <m@naskya.net>
Date: Sat, 20 Apr 2024 23:04:12 +0900
Subject: [PATCH 097/110] chore (backend): fix imports

---
 packages/backend/src/misc/cache.ts            |  2 +-
 packages/backend/src/server/activitypub.ts    |  2 +-
 .../src/server/file/byte-range-readable.ts    |  2 +-
 .../src/services/chart/charts/active-users.ts |  1 -
 packages/backend/src/services/fetch-rel-me.ts |  1 +
 packages/backend/src/services/note/read.ts    | 31 -------------------
 6 files changed, 4 insertions(+), 35 deletions(-)

diff --git a/packages/backend/src/misc/cache.ts b/packages/backend/src/misc/cache.ts
index 30a50e5714..e99b17a5f7 100644
--- a/packages/backend/src/misc/cache.ts
+++ b/packages/backend/src/misc/cache.ts
@@ -1,6 +1,6 @@
 import { redisClient } from "@/db/redis.js";
 import { encode, decode } from "msgpackr";
-import { ChainableCommander } from "ioredis";
+import type { ChainableCommander } from "ioredis";
 
 export class Cache<T> {
 	private ttl: number;
diff --git a/packages/backend/src/server/activitypub.ts b/packages/backend/src/server/activitypub.ts
index 00c8a6babe..71d95709b7 100644
--- a/packages/backend/src/server/activitypub.ts
+++ b/packages/backend/src/server/activitypub.ts
@@ -32,7 +32,7 @@ import Followers from "./activitypub/followers.js";
 import Outbox, { packActivity } from "./activitypub/outbox.js";
 import { serverLogger } from "./index.js";
 import config from "@/config/index.js";
-import Koa from "koa";
+import type Koa from "koa";
 import * as crypto from "node:crypto";
 import { inspect } from "node:util";
 import type { IActivity } from "@/remote/activitypub/type.js";
diff --git a/packages/backend/src/server/file/byte-range-readable.ts b/packages/backend/src/server/file/byte-range-readable.ts
index 96dcbc4a52..9699f95092 100644
--- a/packages/backend/src/server/file/byte-range-readable.ts
+++ b/packages/backend/src/server/file/byte-range-readable.ts
@@ -1,4 +1,4 @@
-import { Readable, ReadableOptions } from "node:stream";
+import { Readable, type ReadableOptions } from "node:stream";
 import { Buffer } from "node:buffer";
 import * as fs from "node:fs";
 
diff --git a/packages/backend/src/services/chart/charts/active-users.ts b/packages/backend/src/services/chart/charts/active-users.ts
index 3f4b7e3381..067334005e 100644
--- a/packages/backend/src/services/chart/charts/active-users.ts
+++ b/packages/backend/src/services/chart/charts/active-users.ts
@@ -1,4 +1,3 @@
-import type { KVs } from "../core.js";
 import Chart from "../core.js";
 import type { User } from "@/models/entities/user.js";
 import { name, schema } from "./entities/active-users.js";
diff --git a/packages/backend/src/services/fetch-rel-me.ts b/packages/backend/src/services/fetch-rel-me.ts
index c9a37d1c88..70faa01aa7 100644
--- a/packages/backend/src/services/fetch-rel-me.ts
+++ b/packages/backend/src/services/fetch-rel-me.ts
@@ -1,4 +1,5 @@
 import { Window } from "happy-dom";
+import type { HTMLAnchorElement, HTMLLinkElement } from "happy-dom";
 import config from "@/config/index.js";
 
 async function getRelMeLinks(url: string): Promise<string[]> {
diff --git a/packages/backend/src/services/note/read.ts b/packages/backend/src/services/note/read.ts
index 3c49501416..7eab66c41b 100644
--- a/packages/backend/src/services/note/read.ts
+++ b/packages/backend/src/services/note/read.ts
@@ -3,7 +3,6 @@ import type { Note } from "@/models/entities/note.js";
 import type { User } from "@/models/entities/user.js";
 import {
 	NoteUnreads,
-	Users,
 	Followings,
 	ChannelFollowings,
 } from "@/models/index.js";
@@ -120,34 +119,4 @@ export default async function (
 			]),
 		});
 	}
-
-	// if (readAntennaNotes.length > 0) {
-	// 	await AntennaNotes.update(
-	// 		{
-	// 			antennaId: In(myAntennas.map((a) => a.id)),
-	// 			noteId: In(readAntennaNotes.map((n) => n.id)),
-	// 		},
-	// 		{
-	// 			read: true,
-	// 		},
-	// 	);
-
-	// 	// TODO: まとめてクエリしたい
-	// 	for (const antenna of myAntennas) {
-	// 		const count = await AntennaNotes.countBy({
-	// 			antennaId: antenna.id,
-	// 			read: false,
-	// 		});
-
-	// 		if (count === 0) {
-	// 			publishMainStream(userId, "readAntenna", antenna);
-	// 		}
-	// 	}
-
-	// 	Users.getHasUnreadAntenna(userId).then((unread) => {
-	// 		if (!unread) {
-	// 			publishMainStream(userId, "readAllAntennas");
-	// 		}
-	// 	});
-	// }
 }

From 488323cc8ec252447907a4b7e9f19a7b7ef7c24e Mon Sep 17 00:00:00 2001
From: naskya <m@naskya.net>
Date: Sun, 21 Apr 2024 05:57:05 +0900
Subject: [PATCH 098/110] chore: format

---
 packages/backend/src/services/note/read.ts | 6 +-----
 1 file changed, 1 insertion(+), 5 deletions(-)

diff --git a/packages/backend/src/services/note/read.ts b/packages/backend/src/services/note/read.ts
index 7eab66c41b..d7fda27a85 100644
--- a/packages/backend/src/services/note/read.ts
+++ b/packages/backend/src/services/note/read.ts
@@ -1,11 +1,7 @@
 import { publishMainStream } from "@/services/stream.js";
 import type { Note } from "@/models/entities/note.js";
 import type { User } from "@/models/entities/user.js";
-import {
-	NoteUnreads,
-	Followings,
-	ChannelFollowings,
-} from "@/models/index.js";
+import { NoteUnreads, Followings, ChannelFollowings } from "@/models/index.js";
 import { Not, IsNull, In } from "typeorm";
 import type { Channel } from "@/models/entities/channel.js";
 import { readNotificationByQuery } from "@/server/api/common/read-notification.js";

From 2760e7feeeba3d68a8770d3458adf3a53a22dfae Mon Sep 17 00:00:00 2001
From: naskya <m@naskya.net>
Date: Sun, 21 Apr 2024 06:40:53 +0900
Subject: [PATCH 099/110] chore (minor): use ** in lieu of Math.pow

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

diff --git a/packages/backend/src/queue/initialize.ts b/packages/backend/src/queue/initialize.ts
index 0f9c83132f..a874005fbd 100644
--- a/packages/backend/src/queue/initialize.ts
+++ b/packages/backend/src/queue/initialize.ts
@@ -34,7 +34,7 @@ export function initialize<T>(name: string, limitPerSec = -1) {
 function apBackoff(attemptsMade: number, err: Error) {
 	const baseDelay = 60 * 1000; // 1min
 	const maxBackoff = 8 * 60 * 60 * 1000; // 8hours
-	let backoff = (Math.pow(2, attemptsMade) - 1) * baseDelay;
+	let backoff = (2 ** attemptsMade - 1) * baseDelay;
 	backoff = Math.min(backoff, maxBackoff);
 	backoff += Math.round(backoff * Math.random() * 0.2);
 	return backoff;

From dc02a077742bfc81bba089acd69520f189d64fda Mon Sep 17 00:00:00 2001
From: mei23 <m@m544.net>
Date: Sun, 21 Apr 2024 09:29:00 +0900
Subject: [PATCH 100/110] fix (backend): add Cache-Control to Bull Dashboard

---
 packages/backend/src/server/web/index.ts | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/packages/backend/src/server/web/index.ts b/packages/backend/src/server/web/index.ts
index 939fcfab14..4473165be7 100644
--- a/packages/backend/src/server/web/index.ts
+++ b/packages/backend/src/server/web/index.ts
@@ -54,6 +54,10 @@ app.use(async (ctx, next) => {
 	const url = decodeURI(ctx.path);
 
 	if (url === bullBoardPath || url.startsWith(`${bullBoardPath}/`)) {
+		if (!url.startsWith(`${bullBoardPath}/static/`)) {
+			ctx.set("Cache-Control", "private, max-age=0, must-revalidate");
+		}
+
 		const token = ctx.cookies.get("token");
 		if (token == null) {
 			ctx.status = 401;

From d1e898c0d0eb1d9258bffed10816cce510847077 Mon Sep 17 00:00:00 2001
From: naskya <m@naskya.net>
Date: Sun, 21 Apr 2024 09:32:05 +0900
Subject: [PATCH 101/110] docs: update changelog

---
 docs/changelog.md | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/docs/changelog.md b/docs/changelog.md
index a818e09835..860dfbad8c 100644
--- a/docs/changelog.md
+++ b/docs/changelog.md
@@ -5,6 +5,10 @@ Critical security updates are indicated by the :warning: icon.
 - Server administrators should check [notice-for-admins.md](./notice-for-admins.md) as well.
 - Third-party client/bot developers may want to check [api-change.md](./api-change.md) as well.
 
+## Unreleased
+
+- Fix bugs
+
 ## [v20240413](https://firefish.dev/firefish/firefish/-/merge_requests/10741/commits)
 
 - Add "Media" tab to user page

From dac4043dd9bdca046c4a60b1a1662abdfff76dde Mon Sep 17 00:00:00 2001
From: naskya <m@naskya.net>
Date: Sun, 21 Apr 2024 10:09:45 +0900
Subject: [PATCH 102/110] v20240421

---
 docs/changelog.md | 2 +-
 package.json      | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/docs/changelog.md b/docs/changelog.md
index 860dfbad8c..70f8b34fbe 100644
--- a/docs/changelog.md
+++ b/docs/changelog.md
@@ -5,7 +5,7 @@ Critical security updates are indicated by the :warning: icon.
 - Server administrators should check [notice-for-admins.md](./notice-for-admins.md) as well.
 - Third-party client/bot developers may want to check [api-change.md](./api-change.md) as well.
 
-## Unreleased
+## [v20240421](https://firefish.dev/firefish/firefish/-/merge_requests/10756/commits)
 
 - Fix bugs
 
diff --git a/package.json b/package.json
index dd0766bc7b..a62fae09f0 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
 	"name": "firefish",
-	"version": "20240413",
+	"version": "20240421",
 	"repository": {
 		"type": "git",
 		"url": "https://firefish.dev/firefish/firefish.git"

From 9f3396af21bf459394287cdf16656a72065a8b5b Mon Sep 17 00:00:00 2001
From: naskya <m@naskya.net>
Date: Sun, 21 Apr 2024 10:30:13 +0900
Subject: [PATCH 103/110] chore (backend): translate Japanese comments into
 English

---
 packages/backend/src/services/logger.ts | 10 +++++-----
 1 file changed, 5 insertions(+), 5 deletions(-)

diff --git a/packages/backend/src/services/logger.ts b/packages/backend/src/services/logger.ts
index e53279e31c..3fcf28671e 100644
--- a/packages/backend/src/services/logger.ts
+++ b/packages/backend/src/services/logger.ts
@@ -144,12 +144,12 @@ export default class Logger {
 		}
 	}
 
+	// Used when the process can't continue (fatal error)
 	public error(
 		x: string | Error,
 		data?: Record<string, any> | null,
 		important = false,
 	): void {
-		// 実行を継続できない状況で使う
 		if (x instanceof Error) {
 			data = data || {};
 			data.e = x;
@@ -166,30 +166,30 @@ export default class Logger {
 		}
 	}
 
+	// Used when the process can continue but some action should be taken
 	public warn(
 		message: string,
 		data?: Record<string, any> | null,
 		important = false,
 	): void {
-		// 実行を継続できるが改善すべき状況で使う
 		this.log("warning", message, data, important);
 	}
 
+	// Used when something is successful
 	public succ(
 		message: string,
 		data?: Record<string, any> | null,
 		important = false,
 	): void {
-		// 何かに成功した状況で使う
 		this.log("success", message, data, important);
 	}
 
+	// Used for debugging (information necessary for developers but unnecessary for users)
 	public debug(
 		message: string,
 		data?: Record<string, any> | null,
 		important = false,
 	): void {
-		// Used for debugging (information necessary for developers but unnecessary for users)
 		// Fixed if statement is ignored when logLevel includes debug
 		if (
 			config.logLevel?.includes("debug") ||
@@ -200,12 +200,12 @@ export default class Logger {
 		}
 	}
 
+	// Other generic logs
 	public info(
 		message: string,
 		data?: Record<string, any> | null,
 		important = false,
 	): void {
-		// それ以外
 		this.log("info", message, data, important);
 	}
 }

From 28f7ac1acd835ea2059289b060ec6b74b7609929 Mon Sep 17 00:00:00 2001
From: naskya <m@naskya.net>
Date: Sun, 21 Apr 2024 10:31:00 +0900
Subject: [PATCH 104/110] fix (backend): typo

---
 packages/backend/src/services/logger.ts | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/packages/backend/src/services/logger.ts b/packages/backend/src/services/logger.ts
index 3fcf28671e..f4b4454ef8 100644
--- a/packages/backend/src/services/logger.ts
+++ b/packages/backend/src/services/logger.ts
@@ -28,9 +28,9 @@ export default class Logger {
 
 		if (config.syslog) {
 			this.syslogClient = new SyslogPro.RFC5424({
-				applacationName: "Firefish",
+				applicationName: "Firefish",
 				timestamp: true,
-				encludeStructuredData: true,
+				includeStructuredData: true,
 				color: true,
 				extendedColor: true,
 				server: {

From 43570a54aa3e146698487d5ee39686f428b91397 Mon Sep 17 00:00:00 2001
From: naskya <m@naskya.net>
Date: Sun, 21 Apr 2024 10:44:54 +0900
Subject: [PATCH 105/110] chore: format

---
 packages/backend/src/services/drive/upload-from-url.ts | 5 ++++-
 1 file changed, 4 insertions(+), 1 deletion(-)

diff --git a/packages/backend/src/services/drive/upload-from-url.ts b/packages/backend/src/services/drive/upload-from-url.ts
index a96e8e3262..e7b084bda1 100644
--- a/packages/backend/src/services/drive/upload-from-url.ts
+++ b/packages/backend/src/services/drive/upload-from-url.ts
@@ -3,7 +3,10 @@ import type { User } from "@/models/entities/user.js";
 import { createTemp } from "@/misc/create-temp.js";
 import { downloadUrl, isPrivateIp } from "@/misc/download-url.js";
 import type { DriveFolder } from "@/models/entities/drive-folder.js";
-import type { DriveFile, DriveFileUsageHint } from "@/models/entities/drive-file.js";
+import type {
+	DriveFile,
+	DriveFileUsageHint,
+} from "@/models/entities/drive-file.js";
 import { DriveFiles } from "@/models/index.js";
 import { driveLogger } from "./logger.js";
 import { addFile } from "./add-file.js";

From c936102a4c95b4b6e12b3caf46ef9bcdde4f4e2c Mon Sep 17 00:00:00 2001
From: naskya <m@naskya.net>
Date: Sun, 21 Apr 2024 10:45:47 +0900
Subject: [PATCH 106/110] chore (backend-rs): regenerate entities and
 index.js/d.ts

---
 packages/backend-rs/index.d.ts                     |  5 +++++
 packages/backend-rs/index.js                       |  3 ++-
 packages/backend-rs/src/model/entity/drive_file.rs |  3 ++-
 .../src/model/entity/sea_orm_active_enums.rs       | 14 ++++++++++++++
 4 files changed, 23 insertions(+), 2 deletions(-)

diff --git a/packages/backend-rs/index.d.ts b/packages/backend-rs/index.d.ts
index 1cf961bd30..5a7bf218c5 100644
--- a/packages/backend-rs/index.d.ts
+++ b/packages/backend-rs/index.d.ts
@@ -348,6 +348,7 @@ export interface DriveFile {
   webpublicType: string | null
   requestHeaders: Json | null
   requestIp: string | null
+  usageHint: DriveFileUsageHintEnum | null
 }
 export interface DriveFolder {
   id: string
@@ -780,6 +781,10 @@ export enum AntennaSrcEnum {
   List = 'list',
   Users = 'users'
 }
+export enum DriveFileUsageHintEnum {
+  UserAvatar = 'userAvatar',
+  UserBanner = 'userBanner'
+}
 export enum MutedNoteReasonEnum {
   Manual = 'manual',
   Other = 'other',
diff --git a/packages/backend-rs/index.js b/packages/backend-rs/index.js
index 1ea7bb5bed..6d64f6ec75 100644
--- a/packages/backend-rs/index.js
+++ b/packages/backend-rs/index.js
@@ -310,7 +310,7 @@ if (!nativeBinding) {
   throw new Error(`Failed to load native binding`)
 }
 
-const { readEnvironmentConfig, readServerConfig, stringToAcct, acctToString, checkWordMute, getFullApAccount, isSelfHost, isSameOrigin, extractHost, toPuny, isUnicodeEmoji, sqlLikeEscape, safeForSql, formatMilliseconds, getNoteSummary, toMastodonId, fromMastodonId, fetchMeta, metaToPugArgs, nyaify, hashPassword, verifyPassword, isOldPasswordAlgorithm, decodeReaction, countReactions, toDbReaction, AntennaSrcEnum, MutedNoteReasonEnum, NoteVisibilityEnum, NotificationTypeEnum, PageVisibilityEnum, PollNotevisibilityEnum, RelayStatusEnum, UserEmojimodpermEnum, UserProfileFfvisibilityEnum, UserProfileMutingnotificationtypesEnum, initIdGenerator, getTimestamp, genId, secureRndstr } = nativeBinding
+const { readEnvironmentConfig, readServerConfig, stringToAcct, acctToString, checkWordMute, getFullApAccount, isSelfHost, isSameOrigin, extractHost, toPuny, isUnicodeEmoji, sqlLikeEscape, safeForSql, formatMilliseconds, getNoteSummary, toMastodonId, fromMastodonId, fetchMeta, metaToPugArgs, nyaify, hashPassword, verifyPassword, isOldPasswordAlgorithm, decodeReaction, countReactions, toDbReaction, AntennaSrcEnum, DriveFileUsageHintEnum, MutedNoteReasonEnum, NoteVisibilityEnum, NotificationTypeEnum, PageVisibilityEnum, PollNotevisibilityEnum, RelayStatusEnum, UserEmojimodpermEnum, UserProfileFfvisibilityEnum, UserProfileMutingnotificationtypesEnum, initIdGenerator, getTimestamp, genId, secureRndstr } = nativeBinding
 
 module.exports.readEnvironmentConfig = readEnvironmentConfig
 module.exports.readServerConfig = readServerConfig
@@ -339,6 +339,7 @@ module.exports.decodeReaction = decodeReaction
 module.exports.countReactions = countReactions
 module.exports.toDbReaction = toDbReaction
 module.exports.AntennaSrcEnum = AntennaSrcEnum
+module.exports.DriveFileUsageHintEnum = DriveFileUsageHintEnum
 module.exports.MutedNoteReasonEnum = MutedNoteReasonEnum
 module.exports.NoteVisibilityEnum = NoteVisibilityEnum
 module.exports.NotificationTypeEnum = NotificationTypeEnum
diff --git a/packages/backend-rs/src/model/entity/drive_file.rs b/packages/backend-rs/src/model/entity/drive_file.rs
index e5c3995573..a6926e7af2 100644
--- a/packages/backend-rs/src/model/entity/drive_file.rs
+++ b/packages/backend-rs/src/model/entity/drive_file.rs
@@ -1,5 +1,6 @@
 //! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.15
 
+use super::sea_orm_active_enums::DriveFileUsageHintEnum;
 use sea_orm::entity::prelude::*;
 
 #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
@@ -53,7 +54,7 @@ pub struct Model {
     #[sea_orm(column_name = "requestIp")]
     pub request_ip: Option<String>,
     #[sea_orm(column_name = "usageHint")]
-    pub usage_hint: Option<String>,
+    pub usage_hint: Option<DriveFileUsageHintEnum>,
 }
 
 #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
diff --git a/packages/backend-rs/src/model/entity/sea_orm_active_enums.rs b/packages/backend-rs/src/model/entity/sea_orm_active_enums.rs
index 38820e1bd8..36281f4dc5 100644
--- a/packages/backend-rs/src/model/entity/sea_orm_active_enums.rs
+++ b/packages/backend-rs/src/model/entity/sea_orm_active_enums.rs
@@ -23,6 +23,20 @@ pub enum AntennaSrcEnum {
 #[derive(Debug, PartialEq, Eq, EnumIter, DeriveActiveEnum)]
 #[cfg_attr(not(feature = "napi"), derive(Clone))]
 #[cfg_attr(feature = "napi", napi_derive::napi(string_enum = "camelCase"))]
+#[sea_orm(
+    rs_type = "String",
+    db_type = "Enum",
+    enum_name = "drive_file_usage_hint_enum"
+)]
+pub enum DriveFileUsageHintEnum {
+    #[sea_orm(string_value = "userAvatar")]
+    UserAvatar,
+    #[sea_orm(string_value = "userBanner")]
+    UserBanner,
+}
+#[derive(Debug, PartialEq, Eq, EnumIter, DeriveActiveEnum)]
+#[cfg_attr(not(feature = "napi"), derive(Clone))]
+#[cfg_attr(feature = "napi", napi_derive::napi(string_enum = "camelCase"))]
 #[sea_orm(
     rs_type = "String",
     db_type = "Enum",

From 96481f1353dd665b0c05d776d5df54f6ce2a57a8 Mon Sep 17 00:00:00 2001
From: naskya <m@naskya.net>
Date: Sun, 21 Apr 2024 10:48:31 +0900
Subject: [PATCH 107/110] chore: update downgrade.sql

---
 docs/downgrade.sql | 7 ++++++-
 1 file changed, 6 insertions(+), 1 deletion(-)

diff --git a/docs/downgrade.sql b/docs/downgrade.sql
index 77dea27573..44222f818f 100644
--- a/docs/downgrade.sql
+++ b/docs/downgrade.sql
@@ -1,6 +1,7 @@
 BEGIN;
 
 DELETE FROM "migrations" WHERE name IN (
+    'AddDriveFileUsage1713451569342',
     'ConvertCwVarcharToText1713225866247',
     'FixChatFileConstraint1712855579316',
     'DropTimeZone1712425488543',
@@ -23,7 +24,11 @@ DELETE FROM "migrations" WHERE name IN (
     'RemoveNativeUtilsMigration1705877093218'
 );
 
---convert-cw-varchar-to-text
+-- AddDriveFileUsage
+ALTER TABLE "drive_file" DROP COLUMN "usageHint";
+DROP TYPE "drive_file_usage_hint_enum";
+
+-- convert-cw-varchar-to-text
 DROP INDEX "IDX_8e3bbbeb3df04d1a8105da4c8f";
 ALTER TABLE "note" ALTER COLUMN "cw" TYPE character varying(512);
 CREATE INDEX "IDX_8e3bbbeb3df04d1a8105da4c8f" ON "note" USING "pgroonga" ("cw" pgroonga_varchar_full_text_search_ops_v2);

From d2dbfb37c72234e4b4a3b23867c48ec9034861b3 Mon Sep 17 00:00:00 2001
From: naskya <m@naskya.net>
Date: Sun, 21 Apr 2024 10:59:02 +0900
Subject: [PATCH 108/110] chore (backend): reflect entity changes to the schema
 and repository

---
 packages/backend/src/models/repositories/drive-file.ts | 2 ++
 packages/backend/src/models/schema/drive-file.ts       | 6 ++++++
 2 files changed, 8 insertions(+)

diff --git a/packages/backend/src/models/repositories/drive-file.ts b/packages/backend/src/models/repositories/drive-file.ts
index 2321f20d4c..18b139caff 100644
--- a/packages/backend/src/models/repositories/drive-file.ts
+++ b/packages/backend/src/models/repositories/drive-file.ts
@@ -152,6 +152,7 @@ export const DriveFileRepository = db.getRepository(DriveFile).extend({
 			md5: file.md5,
 			size: file.size,
 			isSensitive: file.isSensitive,
+			usageHint: file.usageHint,
 			blurhash: file.blurhash,
 			properties: opts.self ? file.properties : this.getPublicProperties(file),
 			url: opts.self ? file.url : this.getPublicUrl(file, false),
@@ -193,6 +194,7 @@ export const DriveFileRepository = db.getRepository(DriveFile).extend({
 			md5: file.md5,
 			size: file.size,
 			isSensitive: file.isSensitive,
+			usageHint: file.usageHint,
 			blurhash: file.blurhash,
 			properties: opts.self ? file.properties : this.getPublicProperties(file),
 			url: opts.self ? file.url : this.getPublicUrl(file, false),
diff --git a/packages/backend/src/models/schema/drive-file.ts b/packages/backend/src/models/schema/drive-file.ts
index 30db9e7d48..929dbb472e 100644
--- a/packages/backend/src/models/schema/drive-file.ts
+++ b/packages/backend/src/models/schema/drive-file.ts
@@ -44,6 +44,12 @@ export const packedDriveFileSchema = {
 			optional: false,
 			nullable: false,
 		},
+		usageHint: {
+			type: "string",
+			optional: false,
+			nullable: true,
+			enum: ["userAvatar", "userBanner"],
+		},
 		blurhash: {
 			type: "string",
 			optional: false,

From 6b008c651a899a47b202f1d25b6c5ed56577e962 Mon Sep 17 00:00:00 2001
From: naskya <m@naskya.net>
Date: Sun, 21 Apr 2024 11:09:18 +0900
Subject: [PATCH 109/110] chore (backend): remove (technically) incorrect
 TypeORM decorator field

---
 packages/backend/src/models/entities/drive-file.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/packages/backend/src/models/entities/drive-file.ts b/packages/backend/src/models/entities/drive-file.ts
index 2c6c1bf598..81f564115f 100644
--- a/packages/backend/src/models/entities/drive-file.ts
+++ b/packages/backend/src/models/entities/drive-file.ts
@@ -179,11 +179,11 @@ export class DriveFile {
 	})
 	public isSensitive: boolean;
 
+	// Hint for what this file is used for
 	@Column({
 		type: "enum",
 		enum: ["userAvatar", "userBanner"],
 		nullable: true,
-		comment: "Hint for what the file is used for.",
 	})
 	public usageHint: DriveFileUsageHint;
 

From ce672f4edd542d78a1b33429d379ec969ae163cb Mon Sep 17 00:00:00 2001
From: naskya <m@naskya.net>
Date: Sun, 21 Apr 2024 22:36:05 +0900
Subject: [PATCH 110/110] dev: add cargo test to pnpm scripts

mocha test has been unmaintained for a long time and is very broken :(
---
 package.json | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/package.json b/package.json
index a62fae09f0..93594a5698 100644
--- a/package.json
+++ b/package.json
@@ -26,7 +26,9 @@
 		"debug": "pnpm run build:debug && pnpm run start",
 		"build:debug": "pnpm run clean && pnpm node ./scripts/dev-build.mjs && pnpm run gulp",
 		"mocha": "pnpm --filter backend run mocha",
-		"test": "pnpm run mocha",
+		"test": "pnpm run test:ts && pnpm run test:rs",
+		"test:ts": "pnpm run mocha",
+		"test:rs": "cargo test",
 		"format": "pnpm run format:ts; pnpm run format:rs",
 		"format:ts": "pnpm -r --parallel run format",
 		"format:rs": "cargo fmt --all --",