From ee8679e2398ffa3432a52f9b0fe990939a5a531c Mon Sep 17 00:00:00 2001
From: laozhoubuluo <laozhoubuluo@gmail.com>
Date: Thu, 4 Jul 2024 02:56:16 +0900
Subject: [PATCH 01/17] refactor: use dont federate initially flag instead of
 visibility hack

Co-authored-by: naskya <m@naskya.net>
---
 packages/backend/src/misc/post.ts             |  6 +-
 .../processors/db/import-firefish-post.ts     | 39 +++++----
 .../queue/processors/db/import-masto-post.ts  | 85 +++++++++++++++----
 packages/backend/src/services/note/create.ts  |  7 +-
 4 files changed, 98 insertions(+), 39 deletions(-)

diff --git a/packages/backend/src/misc/post.ts b/packages/backend/src/misc/post.ts
index 0b107ed009..fd89a4d7e3 100644
--- a/packages/backend/src/misc/post.ts
+++ b/packages/backend/src/misc/post.ts
@@ -1,3 +1,5 @@
+import { noteVisibilities } from "@/types.js";
+
 export type Post = {
 	text: string | undefined;
 	cw: string | null;
@@ -12,7 +14,9 @@ export function parse(acct: any): Post {
 		cw: acct.cw,
 		localOnly: acct.localOnly,
 		createdAt: new Date(acct.createdAt),
-		visibility: `hidden${acct.visibility || ""}`,
+		visibility: noteVisibilities.includes(acct.visibility)
+			? acct.visibility
+			: "specified",
 	};
 }
 
diff --git a/packages/backend/src/queue/processors/db/import-firefish-post.ts b/packages/backend/src/queue/processors/db/import-firefish-post.ts
index 34a27c4c7a..8d318a0d2a 100644
--- a/packages/backend/src/queue/processors/db/import-firefish-post.ts
+++ b/packages/backend/src/queue/processors/db/import-firefish-post.ts
@@ -92,22 +92,29 @@ export async function importCkPost(
 		logger.info("Post updated");
 	}
 	if (note == null) {
-		note = await create(user, {
-			createdAt: createdAt,
-			files: files.length === 0 ? undefined : files,
-			poll: undefined,
-			text: text || undefined,
-			reply: post.replyId ? job.data.parent : null,
-			renote: post.renoteId ? job.data.parent : null,
-			cw: cw,
-			localOnly,
-			visibility: visibility,
-			visibleUsers: [],
-			channel: null,
-			apMentions: new Array(0),
-			apHashtags: undefined,
-			apEmojis: undefined,
-		});
+		note = await create(
+			user,
+			{
+				createdAt: createdAt,
+				scheduledAt: undefined,
+				files: files.length === 0 ? undefined : files,
+				poll: undefined,
+				text: text || undefined,
+				reply: post.replyId ? job.data.parent : null,
+				renote: post.renoteId ? job.data.parent : null,
+				cw: cw,
+				localOnly,
+				visibility: visibility,
+				visibleUsers: [],
+				channel: null,
+				apMentions: new Array(0),
+				apHashtags: undefined,
+				apEmojis: undefined,
+			},
+			false,
+			undefined,
+			true,
+		);
 		logger.debug("New post has been created");
 	} else {
 		logger.info("This post already exists");
diff --git a/packages/backend/src/queue/processors/db/import-masto-post.ts b/packages/backend/src/queue/processors/db/import-masto-post.ts
index 532c288dba..f87c44e98e 100644
--- a/packages/backend/src/queue/processors/db/import-masto-post.ts
+++ b/packages/backend/src/queue/processors/db/import-masto-post.ts
@@ -10,6 +10,10 @@ import type { DriveFile } from "@/models/entities/drive-file.js";
 import { Notes, NoteEdits } from "@/models/index.js";
 import type { Note } from "@/models/entities/note.js";
 import { genId } from "backend-rs";
+import promiseLimit from "promise-limit";
+import { unique, concat } from "@/prelude/array.js";
+import type { CacheableUser } from "@/models/entities/user.js";
+import { resolvePerson } from "@/remote/activitypub/models/person.js";
 
 const logger = queueLogger.createSubLogger("import-masto-post");
 
@@ -118,24 +122,57 @@ export async function importMastoPost(
 		logger.info("Post updated");
 	}
 	if (note == null) {
-		note = await create(user, {
-			createdAt: isRenote
-				? new Date(post.published)
-				: new Date(post.object.published),
-			files: files.length === 0 ? undefined : files,
-			poll: undefined,
-			text: text || undefined,
-			reply,
-			renote,
-			cw: !isRenote && post.object.sensitive ? post.object.summary : undefined,
-			localOnly: false,
-			visibility: "hiddenpublic",
-			visibleUsers: [],
-			channel: null,
-			apMentions: new Array(0),
-			apHashtags: undefined,
-			apEmojis: undefined,
-		});
+		let visibility = "specified";
+		let visibleUsers: CacheableUser[] = [];
+		if (isPublic(post.to)) {
+			visibility = "public";
+		} else if (isPublic(post.cc)) {
+			visibility = "home";
+		} else if (isFollowers(post.cc)) {
+			visibility = "followers";
+		} else {
+			try {
+				const visibleUsersList = unique(concat([post.to, post.cc]));
+
+				const limit = promiseLimit<CacheableUser | null>(2);
+				visibleUsers = (
+					await Promise.all(
+						visibleUsersList.map((id) =>
+							limit(() => resolvePerson(id).catch(() => null)),
+						),
+					)
+				).filter((x): x is CacheableUser => x != null);
+			} catch {
+				// nothing need to do.
+			}
+		}
+
+		note = await create(
+			user,
+			{
+				createdAt: isRenote
+					? new Date(post.published)
+					: new Date(post.object.published),
+				scheduledAt: undefined,
+				files: files.length === 0 ? undefined : files,
+				poll: undefined,
+				text: text || undefined,
+				reply,
+				renote,
+				cw:
+					!isRenote && post.object.sensitive ? post.object.summary : undefined,
+				localOnly: false,
+				visibility,
+				visibleUsers,
+				channel: null,
+				apMentions: new Array(0),
+				apHashtags: undefined,
+				apEmojis: undefined,
+			},
+			false,
+			undefined,
+			true,
+		);
 		logger.debug("New post has been created");
 	} else {
 		logger.info("This post already exists");
@@ -145,3 +182,15 @@ export async function importMastoPost(
 
 	logger.info("Imported");
 }
+
+function isPublic(id: string) {
+	return [
+		"https://www.w3.org/ns/activitystreams#Public",
+		"as:Public",
+		"Public",
+	].includes(id);
+}
+
+function isFollowers(id: string) {
+	return id.endsWith("/followers");
+}
diff --git a/packages/backend/src/services/note/create.ts b/packages/backend/src/services/note/create.ts
index 03ca4f7578..c5b71925a0 100644
--- a/packages/backend/src/services/note/create.ts
+++ b/packages/backend/src/services/note/create.ts
@@ -162,11 +162,12 @@ export default async (
 	data: NoteLike,
 	silent = false,
 	waitToPublish?: (note: Note) => Promise<void>,
+	dontFederateInitially = false,
 ) =>
 	// biome-ignore lint/suspicious/noAsyncPromiseExecutor: FIXME
 	new Promise<Note>(async (res, rej) => {
-		const dontFederateInitially =
-			data.visibility?.startsWith("hidden") === true;
+		dontFederateInitially =
+			dontFederateInitially || data.visibility?.startsWith("hidden");
 
 		// Whether this is a scheduled "draft" post (yet to be published)
 		const isDraft = data.scheduledAt != null;
@@ -204,8 +205,6 @@ export default async (
 		if (data.channel != null) data.visibility = "public";
 		if (data.channel != null) data.visibleUsers = [];
 		if (data.channel != null) data.localOnly = true;
-		if (data.visibility.startsWith("hidden") && data.visibility !== "hidden")
-			data.visibility = data.visibility.slice(6);
 
 		// enforce silent clients on server
 		if (

From 1e348f6a7fdca2a05dd130e970860e9f00d76f7e Mon Sep 17 00:00:00 2001
From: naskya <m@naskya.net>
Date: Thu, 4 Jul 2024 02:58:10 +0900
Subject: [PATCH 02/17] chore: reduce code duplication

---
 .../src/queue/processors/db/import-masto-post.ts  | 15 ++-------------
 .../backend/src/remote/activitypub/audience.ts    |  4 ++--
 2 files changed, 4 insertions(+), 15 deletions(-)

diff --git a/packages/backend/src/queue/processors/db/import-masto-post.ts b/packages/backend/src/queue/processors/db/import-masto-post.ts
index f87c44e98e..54308b7e97 100644
--- a/packages/backend/src/queue/processors/db/import-masto-post.ts
+++ b/packages/backend/src/queue/processors/db/import-masto-post.ts
@@ -14,6 +14,7 @@ import promiseLimit from "promise-limit";
 import { unique, concat } from "@/prelude/array.js";
 import type { CacheableUser } from "@/models/entities/user.js";
 import { resolvePerson } from "@/remote/activitypub/models/person.js";
+import { isPublic } from "@/remote/activitypub/audience.js";
 
 const logger = queueLogger.createSubLogger("import-masto-post");
 
@@ -128,7 +129,7 @@ export async function importMastoPost(
 			visibility = "public";
 		} else if (isPublic(post.cc)) {
 			visibility = "home";
-		} else if (isFollowers(post.cc)) {
+		} else if ((post.cc as string).endsWith("/followers")) {
 			visibility = "followers";
 		} else {
 			try {
@@ -182,15 +183,3 @@ export async function importMastoPost(
 
 	logger.info("Imported");
 }
-
-function isPublic(id: string) {
-	return [
-		"https://www.w3.org/ns/activitystreams#Public",
-		"as:Public",
-		"Public",
-	].includes(id);
-}
-
-function isFollowers(id: string) {
-	return id.endsWith("/followers");
-}
diff --git a/packages/backend/src/remote/activitypub/audience.ts b/packages/backend/src/remote/activitypub/audience.ts
index 9d840bc574..ec4ac6d317 100644
--- a/packages/backend/src/remote/activitypub/audience.ts
+++ b/packages/backend/src/remote/activitypub/audience.ts
@@ -90,7 +90,7 @@ function groupingAudience(ids: string[], actor: CacheableRemoteUser) {
 	return groups;
 }
 
-function isPublic(id: string) {
+export function isPublic(id: string) {
 	return [
 		"https://www.w3.org/ns/activitystreams#Public",
 		"as:Public",
@@ -98,6 +98,6 @@ function isPublic(id: string) {
 	].includes(id);
 }
 
-function isFollowers(id: string, actor: CacheableRemoteUser) {
+export function isFollowers(id: string, actor: CacheableRemoteUser) {
 	return id === (actor.followersUri || `${actor.uri}/followers`);
 }

From ee9e2ad60950a0a751f0d6c1499b4a78c435b992 Mon Sep 17 00:00:00 2001
From: laozhoubuluo <laozhoubuluo@gmail.com>
Date: Thu, 4 Jul 2024 03:03:32 +0900
Subject: [PATCH 03/17] fix: unsure string[] -> string

Co-authored-by: naskya <m@naskya.net>
---
 .../backend/src/queue/processors/db/import-masto-post.ts    | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/packages/backend/src/queue/processors/db/import-masto-post.ts b/packages/backend/src/queue/processors/db/import-masto-post.ts
index 54308b7e97..4bf351e8ec 100644
--- a/packages/backend/src/queue/processors/db/import-masto-post.ts
+++ b/packages/backend/src/queue/processors/db/import-masto-post.ts
@@ -125,11 +125,11 @@ export async function importMastoPost(
 	if (note == null) {
 		let visibility = "specified";
 		let visibleUsers: CacheableUser[] = [];
-		if (isPublic(post.to)) {
+		if ((post.to as string[]).some(isPublic)) {
 			visibility = "public";
-		} else if (isPublic(post.cc)) {
+		} else if ((post.cc as string[]).some(isPublic)) {
 			visibility = "home";
-		} else if ((post.cc as string).endsWith("/followers")) {
+		} else if ((post.cc as string[]).some((cc) => cc.endsWith("/followers"))) {
 			visibility = "followers";
 		} else {
 			try {

From f8bc3962ba276b024f8980d3c305cfcb9585d4d1 Mon Sep 17 00:00:00 2001
From: CI <project_7_bot_1bfaee5701aed20091a86249a967a6c1@noreply.firefish.dev>
Date: Wed, 3 Jul 2024 20:05:19 +0000
Subject: [PATCH 04/17] chore(deps): update dependency ws to v8.18.0

---
 packages/backend/package.json |  2 +-
 pnpm-lock.yaml                | 12 ++++++------
 2 files changed, 7 insertions(+), 7 deletions(-)

diff --git a/packages/backend/package.json b/packages/backend/package.json
index a46afb52f4..72b4c1b642 100644
--- a/packages/backend/package.json
+++ b/packages/backend/package.json
@@ -171,6 +171,6 @@
 		"type-fest": "4.21.0",
 		"typescript": "5.5.3",
 		"webpack": "5.92.1",
-		"ws": "8.17.1"
+		"ws": "8.18.0"
 	}
 }
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 98089abd76..3f2ef738c7 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -491,8 +491,8 @@ importers:
         specifier: 5.92.1
         version: 5.92.1
       ws:
-        specifier: 8.17.1
-        version: 8.17.1(bufferutil@4.0.8)(utf-8-validate@5.0.10)
+        specifier: 8.18.0
+        version: 8.18.0(bufferutil@4.0.8)(utf-8-validate@5.0.10)
 
   packages/backend-rs:
     devDependencies:
@@ -7007,8 +7007,8 @@ packages:
       utf-8-validate:
         optional: true
 
-  ws@8.17.1:
-    resolution: {integrity: sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==}
+  ws@8.18.0:
+    resolution: {integrity: sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==}
     engines: {node: '>=10.0.0'}
     peerDependencies:
       bufferutil: ^4.0.1
@@ -11383,7 +11383,7 @@ snapshots:
       whatwg-encoding: 3.1.1
       whatwg-mimetype: 4.0.0
       whatwg-url: 14.0.0
-      ws: 8.17.1(bufferutil@4.0.8)(utf-8-validate@5.0.10)
+      ws: 8.18.0(bufferutil@4.0.8)(utf-8-validate@5.0.10)
       xml-name-validator: 5.0.0
     transitivePeerDependencies:
       - bufferutil
@@ -13625,7 +13625,7 @@ snapshots:
       bufferutil: 4.0.8
       utf-8-validate: 5.0.10
 
-  ws@8.17.1(bufferutil@4.0.8)(utf-8-validate@5.0.10):
+  ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@5.0.10):
     optionalDependencies:
       bufferutil: 4.0.8
       utf-8-validate: 5.0.10

From 603c0e53acdb2366f13ff1254dbfdd519478a728 Mon Sep 17 00:00:00 2001
From: CI <project_7_bot_1bfaee5701aed20091a86249a967a6c1@noreply.firefish.dev>
Date: Wed, 3 Jul 2024 20:05:35 +0000
Subject: [PATCH 05/17] fix(deps): update dependency aws-sdk to v2.1654.0

---
 packages/backend/package.json |  2 +-
 pnpm-lock.yaml                | 10 +++++-----
 2 files changed, 6 insertions(+), 6 deletions(-)

diff --git a/packages/backend/package.json b/packages/backend/package.json
index a46afb52f4..8d7b3ce009 100644
--- a/packages/backend/package.json
+++ b/packages/backend/package.json
@@ -33,7 +33,7 @@
 		"adm-zip": "0.5.14",
 		"ajv": "8.16.0",
 		"archiver": "7.0.1",
-		"aws-sdk": "2.1653.0",
+		"aws-sdk": "2.1654.0",
 		"axios": "1.7.2",
 		"backend-rs": "workspace:*",
 		"blurhash": "2.0.5",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 98089abd76..b23f88fc8d 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -82,8 +82,8 @@ importers:
         specifier: 7.0.1
         version: 7.0.1
       aws-sdk:
-        specifier: 2.1653.0
-        version: 2.1653.0
+        specifier: 2.1654.0
+        version: 2.1654.0
       axios:
         specifier: 1.7.2
         version: 1.7.2
@@ -2935,8 +2935,8 @@ packages:
     resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==}
     engines: {node: '>= 0.4'}
 
-  aws-sdk@2.1653.0:
-    resolution: {integrity: sha512-9f42kuLpMcL1EPZOsLM8u6wlnOMtFwED1b24SN0fBbi/N7N1xTLZ7vbEMt/haz06Lc3Vr3VMDyv0atfMmkboBw==}
+  aws-sdk@2.1654.0:
+    resolution: {integrity: sha512-b5ryvXipBJod9Uh1GUfQNgi5tIIiluxJbyqr/hZ/mr5U8WxrrfjVq3nGnx5JjevFKYRqXIywhumsVyanfACzFA==}
     engines: {node: '>= 10.0.0'}
 
   axios@0.24.0:
@@ -9204,7 +9204,7 @@ snapshots:
     dependencies:
       possible-typed-array-names: 1.0.0
 
-  aws-sdk@2.1653.0:
+  aws-sdk@2.1654.0:
     dependencies:
       buffer: 4.9.2
       events: 1.1.1

From 119e59d06444da617f53d3b525d867d5a5882846 Mon Sep 17 00:00:00 2001
From: naskya <m@naskya.net>
Date: Thu, 4 Jul 2024 12:40:16 +0900
Subject: [PATCH 06/17] docs: update notice-for-admins.md

---
 docs/notice-for-admins.md | 10 +++++++++-
 1 file changed, 9 insertions(+), 1 deletion(-)

diff --git a/docs/notice-for-admins.md b/docs/notice-for-admins.md
index fbc58623fe..d8369050b4 100644
--- a/docs/notice-for-admins.md
+++ b/docs/notice-for-admins.md
@@ -10,7 +10,15 @@ Please take a look at #10947.
 
 ### For all users
 
-This is not related to the recent changes, but we have added a new section called "[Maintain the server](https://firefish.dev/firefish/firefish/-/blob/develop/docs/install.md#maintain-the-server)" in the installation guide. We suggest that you take a look at it. (and we welcome your docs contributions!)
+This is not related to the recent changes, but we have added a new section called "[Maintain the server](https://firefish.dev/firefish/firefish/-/blob/develop/docs/install.md#maintain-the-server)" in the installation guide. We suggest that you take a look at it (and we welcome your docs contributions)!
+
+### For systemd/pm2 users
+
+[Node.js will release a new security fix on July 8th](<https://nodejs.org/en/blog/vulnerability/july-2024-security-releases>). It is highly recommended that you upgrade your Node.js version once it's released.
+
+### For Docker/Podman users
+
+[Node.js will release a new security fix on July 8th](<https://nodejs.org/en/blog/vulnerability/july-2024-security-releases>). Once it's released and the [docker.io/node](<https://hub.docker.com/_/node>) image is updated, we'll rebuild the OCI image based on the new `docker.io/node` image and reupload it as [`registry.firefish.dev/firefish/firefish:latest`](<https://firefish.dev/firefish/firefish/container_registry/1>).
 
 ## v20240607
 

From 1a201a7007a5eb6ecbf9ca10b873dd2bae66a6ae Mon Sep 17 00:00:00 2001
From: naskya <m@naskya.net>
Date: Thu, 4 Jul 2024 12:50:40 +0900
Subject: [PATCH 07/17] style: highlight Lookup button in the post search box

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

diff --git a/packages/client/src/components/MkPostSearch.vue b/packages/client/src/components/MkPostSearch.vue
index 7c732ca2c6..0c56a235ba 100644
--- a/packages/client/src/components/MkPostSearch.vue
+++ b/packages/client/src/components/MkPostSearch.vue
@@ -78,7 +78,7 @@
 				<MkButton inline primary @click="search"
 					>{{ i18n.ts.search }}
 				</MkButton>
-				<MkButton inline @click="lookup">{{ i18n.ts.lookup }}</MkButton>
+				<MkButton inline primary @click="lookup">{{ i18n.ts.lookup }}</MkButton>
 				<MkButton inline @click="cancel">{{ i18n.ts.cancel }}</MkButton>
 			</div>
 		</div>

From 143e40928afc7381d06a7b01accc59faf1e65e16 Mon Sep 17 00:00:00 2001
From: naskya <m@naskya.net>
Date: Thu, 4 Jul 2024 14:20:13 +0900
Subject: [PATCH 08/17] meta: update merge request template

---
 .gitlab/merge_request_templates/default.md | 4 +---
 1 file changed, 1 insertion(+), 3 deletions(-)

diff --git a/.gitlab/merge_request_templates/default.md b/.gitlab/merge_request_templates/default.md
index 6d09072a87..7a37db2c9e 100644
--- a/.gitlab/merge_request_templates/default.md
+++ b/.gitlab/merge_request_templates/default.md
@@ -6,9 +6,7 @@
 
 ## Contribution Guidelines
 By submitting this merge request, you agree to follow our [Contribution Guidelines](https://firefish.dev/firefish/firefish/-/blob/develop/CONTRIBUTING.md)
-- [ ] This closes issue #0000 (please substitute the number)
-- [ ] This is a minor bug fix or refactoring
-
+- [ ] This closes #0000 (please substitute the issue number or open a new one unless this is a minor fix/refactor)
 - [ ] I agree to follow this project's Contribution Guidelines
 - [ ] I have made sure to test this merge request
 - [ ] I have made sure to run `pnpm run format` before submitting this merge request

From f4ae3c7ec3dc10f66c4b229d8aa081682cb3b53b Mon Sep 17 00:00:00 2001
From: naskya <m@naskya.net>
Date: Thu, 4 Jul 2024 14:34:55 +0900
Subject: [PATCH 09/17] meta: update issue templates

---
 .gitlab/issue_templates/default.md  | 2 ++
 .gitlab/issue_templates/feature.md  | 2 ++
 .gitlab/issue_templates/refactor.md | 2 ++
 3 files changed, 6 insertions(+)

diff --git a/.gitlab/issue_templates/default.md b/.gitlab/issue_templates/default.md
index f5823ea666..db5fc81666 100644
--- a/.gitlab/issue_templates/default.md
+++ b/.gitlab/issue_templates/default.md
@@ -90,6 +90,8 @@ By submitting this issue, you agree to follow our [Contribution Guidelines](http
 	However, we are currently so understaffed that it is virtually impossible to
 	respond to every single proposal. So, feel free to implement it if there is no response
 	for more than a week or there is a thumbs-up emoji reaction from the project maintainer(s).
+
+	Many thanks for your involvement!
 -->
 
 
diff --git a/.gitlab/issue_templates/feature.md b/.gitlab/issue_templates/feature.md
index a99265161b..eb12540da1 100644
--- a/.gitlab/issue_templates/feature.md
+++ b/.gitlab/issue_templates/feature.md
@@ -58,6 +58,8 @@ By submitting this issue, you agree to follow our [Contribution Guidelines](http
 	However, we are currently so understaffed that it is virtually impossible to
 	respond to every single proposal. So, feel free to implement it if there is no response
 	for more than a week or there is a thumbs-up emoji reaction from the project maintainer(s).
+
+	Many thanks for your involvement!
 -->
 
 
diff --git a/.gitlab/issue_templates/refactor.md b/.gitlab/issue_templates/refactor.md
index e9f9ffe05f..33b0077403 100644
--- a/.gitlab/issue_templates/refactor.md
+++ b/.gitlab/issue_templates/refactor.md
@@ -58,6 +58,8 @@ By submitting this issue, you agree to follow our [Contribution Guidelines](http
 	However, we are currently so understaffed that it is virtually impossible to
 	respond to every single proposal. So, feel free to implement it if there is no response
 	for more than a week or there is a thumbs-up emoji reaction from the project maintainer(s).
+
+	Many thanks for your involvement!
 -->
 
 

From 71ebea0cd1828cfef59d690ccc225b211383301a Mon Sep 17 00:00:00 2001
From: naskya <m@naskya.net>
Date: Thu, 4 Jul 2024 16:46:38 +0900
Subject: [PATCH 10/17] fix (client): set default values of the instance data

---
 packages/backend/src/boot/master.ts |  3 +-
 packages/client/src/instance.ts     | 64 ++++++++++++++++++++++++++---
 2 files changed, 61 insertions(+), 6 deletions(-)

diff --git a/packages/backend/src/boot/master.ts b/packages/backend/src/boot/master.ts
index dbf51c08c5..801d2cf5fa 100644
--- a/packages/backend/src/boot/master.ts
+++ b/packages/backend/src/boot/master.ts
@@ -25,10 +25,11 @@ export async function masterMain() {
 	// initialize app
 	try {
 		greet();
-		showEnvironment();
 		showServerInfo();
+		showEnvironment();
 		showNodejsVersion();
 		await connectDb();
+		await updateMetaCache();
 	} catch (e) {
 		bootLogger.error(
 			`Fatal error occurred during initialization:\n${inspect(e)}`,
diff --git a/packages/client/src/instance.ts b/packages/client/src/instance.ts
index f4ea4d431e..4092a901c0 100644
--- a/packages/client/src/instance.ts
+++ b/packages/client/src/instance.ts
@@ -6,7 +6,65 @@ import { set, get } from "idb-keyval";
 // TODO: 他のタブと永続化されたstateを同期
 
 // TODO: get("instance") requires top-level await
-let instance: entities.DetailedInstanceMetadata;
+// TODO: fallback to defaults more nicely
+// default values
+let instance: entities.DetailedInstanceMetadata = {
+	maintainerName: "",
+	maintainerEmail: "",
+	version: "",
+	name: null,
+	uri: "",
+	tosUrl: null,
+	description: null,
+	disableRegistration: true,
+	disableLocalTimeline: false,
+	disableGlobalTimeline: false,
+	disableRecommendedTimeline: true,
+	enableGuestTimeline: false,
+	driveCapacityPerLocalUserMb: 1000,
+	driveCapacityPerRemoteUserMb: 0,
+	antennaLimit: 5,
+	enableHcaptcha: false,
+	hcaptchaSiteKey: null,
+	enableRecaptcha: false,
+	recaptchaSiteKey: null,
+	swPublickey: null,
+	maxNoteTextLength: 3000,
+	maxCaptionTextLength: 1500,
+	enableEmail: false,
+	enableServiceWorker: false,
+	markLocalFilesNsfwByDefault: false,
+	emojis: [],
+	ads: [],
+	langs: [],
+	moreUrls: {},
+	repositoryUrl: "https://firefish.dev/firefish/firefish",
+	feedbackUrl: "https://firefish.dev/firefish/firefish/-/issues",
+	defaultDarkTheme: null,
+	defaultLightTheme: null,
+	defaultReaction: "⭐",
+	cacheRemoteFiles: false,
+	proxyAccountName: null,
+	emailRequiredForSignup: false,
+	mascotImageUrl: "",
+	bannerUrl: "",
+	backgroundImageUrl: "",
+	errorImageUrl: "",
+	iconUrl: null,
+	requireSetup: false,
+	translatorAvailable: false,
+	features: {
+		registration: false,
+		localTimeLine: true,
+		recommendedTimeLine: false,
+		globalTimeLine: true,
+		searchFilters: true,
+		hcaptcha: false,
+		recaptcha: false,
+		objectStorage: false,
+		serviceWorker: false,
+	},
+};
 
 export function getInstanceInfo(): entities.DetailedInstanceMetadata {
 	return instance;
@@ -27,13 +85,9 @@ export async function updateInstanceCache(): Promise<void> {
 		detail: true,
 	});
 
-	// TODO: set default values
-	instance = {} as entities.DetailedInstanceMetadata;
-
 	for (const [k, v] of Object.entries(meta)) {
 		instance[k] = v;
 	}
-
 	set("instance", JSON.stringify(instance));
 }
 

From 28576b518ab8ca054ae6623d484d080d4572e739 Mon Sep 17 00:00:00 2001
From: naskya <m@naskya.net>
Date: Thu, 4 Jul 2024 16:46:38 +0900
Subject: [PATCH 11/17] fix (client): missing variables

---
 packages/client/src/ui/visitor/b.vue | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/packages/client/src/ui/visitor/b.vue b/packages/client/src/ui/visitor/b.vue
index fb8bfe5aaf..0d1f6a77d5 100644
--- a/packages/client/src/ui/visitor/b.vue
+++ b/packages/client/src/ui/visitor/b.vue
@@ -110,6 +110,10 @@ provideMetadataReceiver((info) => {
 });
 
 const root = computed(() => mainRouter.currentRoute.value?.name === "index");
+const showMenu = ref(false);
+const isDesktop = ref(window.innerWidth >= DESKTOP_THRESHOLD);
+const narrow = ref(window.innerWidth < 1280);
+const meta = ref();
 
 os.api("meta", { detail: true }).then((res) => {
 	meta.value = res;

From 1e5d63b6179cd44d58b04c675eb08276b49a2695 Mon Sep 17 00:00:00 2001
From: naskya <m@naskya.net>
Date: Thu, 4 Jul 2024 16:46:38 +0900
Subject: [PATCH 12/17] chore (client): rename instance variable

---
 packages/client/src/instance.ts | 24 ++++++++++++------------
 1 file changed, 12 insertions(+), 12 deletions(-)

diff --git a/packages/client/src/instance.ts b/packages/client/src/instance.ts
index 4092a901c0..dd0c98fbd6 100644
--- a/packages/client/src/instance.ts
+++ b/packages/client/src/instance.ts
@@ -5,10 +5,9 @@ import { set, get } from "idb-keyval";
 
 // TODO: 他のタブと永続化されたstateを同期
 
-// TODO: get("instance") requires top-level await
-// TODO: fallback to defaults more nicely
+// TODO: fallback to defaults more nicely (with #10947)
 // default values
-let instance: entities.DetailedInstanceMetadata = {
+let instanceMeta: entities.DetailedInstanceMetadata = {
 	maintainerName: "",
 	maintainerEmail: "",
 	version: "",
@@ -66,15 +65,16 @@ let instance: entities.DetailedInstanceMetadata = {
 	},
 };
 
+// get("instanceMeta") requires top-level await
 export function getInstanceInfo(): entities.DetailedInstanceMetadata {
-	return instance;
+	return instanceMeta;
 }
 
 export async function initializeInstanceCache(): Promise<void> {
 	// Is the data stored in IndexDB?
-	const fromIdb = await get<string>("instance");
+	const fromIdb = await get<string>("instanceMeta");
 	if (fromIdb != null) {
-		instance = JSON.parse(fromIdb);
+		instanceMeta = JSON.parse(fromIdb);
 	}
 	// Call API
 	updateInstanceCache();
@@ -86,24 +86,24 @@ export async function updateInstanceCache(): Promise<void> {
 	});
 
 	for (const [k, v] of Object.entries(meta)) {
-		instance[k] = v;
+		instanceMeta[k] = v;
 	}
-	set("instance", JSON.stringify(instance));
+	set("instanceMeta", JSON.stringify(instanceMeta));
 }
 
 export const emojiCategories = computed(() => {
-	if (instance.emojis == null) return [];
+	if (instanceMeta.emojis == null) return [];
 	const categories = new Set();
-	for (const emoji of instance.emojis) {
+	for (const emoji of instanceMeta.emojis) {
 		categories.add(emoji.category);
 	}
 	return Array.from(categories);
 });
 
 export const emojiTags = computed(() => {
-	if (instance.emojis == null) return [];
+	if (instanceMeta.emojis == null) return [];
 	const tags = new Set();
-	for (const emoji of instance.emojis) {
+	for (const emoji of instanceMeta.emojis) {
 		for (const tag of emoji.aliases) {
 			tags.add(tag);
 		}

From 32573b8f32a0e6b772180d993e5aa20ed7427dbd Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Thu, 4 Jul 2024 16:46:38 +0900
Subject: [PATCH 13/17] chore (client): make sure to remove splash screen DOM
 element

https://github.com/misskey-dev/misskey/commit/c23c97d303d227f6a0e2b3c775189f90772c1dfa

Co-authored-by: naskya <m@naskya.net>
---
 packages/client/src/init.ts | 6 ++++--
 1 file changed, 4 insertions(+), 2 deletions(-)

diff --git a/packages/client/src/init.ts b/packages/client/src/init.ts
index 16750c26e0..51d837c959 100644
--- a/packages/client/src/init.ts
+++ b/packages/client/src/init.ts
@@ -61,9 +61,11 @@ function checkForSplash() {
 	if (splash) {
 		splash.style.opacity = "0";
 		splash.style.pointerEvents = "none";
-		splash.addEventListener("transitionend", () => {
+
+		// remove splash screen
+		window.setTimeout(() => {
 			splash.remove();
-		});
+		}, 1000);
 	}
 }
 

From 9aaafce3b6b490a73d9776eb2418fd81b65f5cf4 Mon Sep 17 00:00:00 2001
From: naskya <m@naskya.net>
Date: Thu, 4 Jul 2024 16:46:39 +0900
Subject: [PATCH 14/17] fix (client, firefish-js): fix type of moreUrls

---
 packages/client/src/instance.ts      | 2 +-
 packages/firefish-js/src/entities.ts | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/packages/client/src/instance.ts b/packages/client/src/instance.ts
index dd0c98fbd6..10249d909f 100644
--- a/packages/client/src/instance.ts
+++ b/packages/client/src/instance.ts
@@ -36,7 +36,7 @@ let instanceMeta: entities.DetailedInstanceMetadata = {
 	emojis: [],
 	ads: [],
 	langs: [],
-	moreUrls: {},
+	moreUrls: [],
 	repositoryUrl: "https://firefish.dev/firefish/firefish",
 	feedbackUrl: "https://firefish.dev/firefish/firefish/-/issues",
 	defaultDarkTheme: null,
diff --git a/packages/firefish-js/src/entities.ts b/packages/firefish-js/src/entities.ts
index 7da946ba95..2541b255c9 100644
--- a/packages/firefish-js/src/entities.ts
+++ b/packages/firefish-js/src/entities.ts
@@ -395,7 +395,7 @@ export type DetailedInstanceMetadata = LiteInstanceMetadata & {
 		miauth?: boolean;
 	};
 	langs: string[];
-	moreUrls: object;
+	moreUrls: { name: string; url: string }[];
 	repositoryUrl: string;
 	feedbackUrl: string;
 	defaultDarkTheme: string | null;

From fa9a004f27164ee1136bd21ecd17b29444bf5a51 Mon Sep 17 00:00:00 2001
From: CI <project_7_bot_1bfaee5701aed20091a86249a967a6c1@noreply.firefish.dev>
Date: Thu, 4 Jul 2024 12:05:03 +0000
Subject: [PATCH 15/17] fix(deps): update dependency @redocly/openapi-core to
 v1.17.1

---
 packages/backend/package.json |  2 +-
 pnpm-lock.yaml                | 10 +++++-----
 2 files changed, 6 insertions(+), 6 deletions(-)

diff --git a/packages/backend/package.json b/packages/backend/package.json
index 32f519c526..c8ec5030e9 100644
--- a/packages/backend/package.json
+++ b/packages/backend/package.json
@@ -28,7 +28,7 @@
 		"@koa/router": "12.0.1",
 		"@ladjs/koa-views": "9.0.0",
 		"@peertube/http-signature": "1.7.0",
-		"@redocly/openapi-core": "1.17.0",
+		"@redocly/openapi-core": "1.17.1",
 		"@sinonjs/fake-timers": "11.2.2",
 		"adm-zip": "0.5.14",
 		"ajv": "8.16.0",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 90f731111f..b84512d886 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -67,8 +67,8 @@ importers:
         specifier: 1.7.0
         version: 1.7.0
       '@redocly/openapi-core':
-        specifier: 1.17.0
-        version: 1.17.0
+        specifier: 1.17.1
+        version: 1.17.1
       '@sinonjs/fake-timers':
         specifier: 11.2.2
         version: 11.2.2
@@ -2177,8 +2177,8 @@ packages:
   '@redocly/config@0.6.2':
     resolution: {integrity: sha512-c3K5u64eMnr2ootPcpEI0ioIRLE8QP8ptvLxG9MwAmb2sU8HMRfVwXDU3AZiMVY2w4Ts0mDc+Xv4HTIk8DRqFw==}
 
-  '@redocly/openapi-core@1.17.0':
-    resolution: {integrity: sha512-XoNIuksnOGAzAcfpyJkHrMxwurXaQfglnovNE7/pTx4OEjik3OT91+tKAyRCkklVCdMtAA3YokGMZzdhjViUWA==}
+  '@redocly/openapi-core@1.17.1':
+    resolution: {integrity: sha512-PQxDLLNk5cBatJBBxvfk49HFw/nVozw1XZ6Dw/GX0Tviq+WxeEjEuLAKfnLVvb5L0wgs4TNmVG4Y+JyofSPu1A==}
     engines: {node: '>=14.19.0', npm: '>=7.0.0'}
 
   '@rollup/plugin-alias@5.1.0':
@@ -8390,7 +8390,7 @@ snapshots:
 
   '@redocly/config@0.6.2': {}
 
-  '@redocly/openapi-core@1.17.0':
+  '@redocly/openapi-core@1.17.1':
     dependencies:
       '@redocly/ajv': 8.11.0
       '@redocly/config': 0.6.2

From 1d03376d9e08aa7230c412fd8657e69589a59167 Mon Sep 17 00:00:00 2001
From: CI <project_7_bot_1bfaee5701aed20091a86249a967a6c1@noreply.firefish.dev>
Date: Thu, 4 Jul 2024 12:05:34 +0000
Subject: [PATCH 16/17] fix(deps): update dependency bull to v4.15.1

---
 packages/backend/package.json |  2 +-
 pnpm-lock.yaml                | 10 +++++-----
 2 files changed, 6 insertions(+), 6 deletions(-)

diff --git a/packages/backend/package.json b/packages/backend/package.json
index 32f519c526..29dad7e06a 100644
--- a/packages/backend/package.json
+++ b/packages/backend/package.json
@@ -37,7 +37,7 @@
 		"axios": "1.7.2",
 		"backend-rs": "workspace:*",
 		"blurhash": "2.0.5",
-		"bull": "4.15.0",
+		"bull": "4.15.1",
 		"cacheable-lookup": "git+https://github.com/TheEssem/cacheable-lookup.git#dd2fb616366a3c68dcf321a57a67295967b204bf",
 		"cbor-x": "1.5.9",
 		"chalk": "5.3.0",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 90f731111f..0ac6a43005 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -94,8 +94,8 @@ importers:
         specifier: 2.0.5
         version: 2.0.5
       bull:
-        specifier: 4.15.0
-        version: 4.15.0
+        specifier: 4.15.1
+        version: 4.15.1
       cacheable-lookup:
         specifier: git+https://github.com/TheEssem/cacheable-lookup.git#dd2fb616366a3c68dcf321a57a67295967b204bf
         version: https://codeload.github.com/TheEssem/cacheable-lookup/tar.gz/dd2fb616366a3c68dcf321a57a67295967b204bf
@@ -3080,8 +3080,8 @@ packages:
     resolution: {integrity: sha512-4T53u4PdgsXqKaIctwF8ifXlRTTmEPJ8iEPWFdGZvcf7sbwYo6FKFEX9eNNAnzFZ7EzJAQ3CJeOtCRA4rDp7Pw==}
     engines: {node: '>=6.14.2'}
 
-  bull@4.15.0:
-    resolution: {integrity: sha512-nOEAfUXwUXtFbRPQP3bWCwpQ/NAerAu2Nym/ucv5C1E+Qh2x6RGdKKsYIfZam4mYncayTynTUN/HLhRgGi2N8w==}
+  bull@4.15.1:
+    resolution: {integrity: sha512-knVKiZdrXbRkB+fWqNryDz85b3JfsT3dBrZexkztwvTH/AFmpHvsC933VB3JX18aJCz47E+xdO57xbDvxljoAg==}
     engines: {node: '>=12'}
 
   busboy@1.6.0:
@@ -9407,7 +9407,7 @@ snapshots:
     dependencies:
       node-gyp-build: 4.8.1
 
-  bull@4.15.0:
+  bull@4.15.1:
     dependencies:
       cron-parser: 4.9.0
       get-port: 5.1.1

From c855681b0c7c41afa9fef925970be83a101874fb Mon Sep 17 00:00:00 2001
From: naskya <m@naskya.net>
Date: Fri, 5 Jul 2024 01:24:14 +0900
Subject: [PATCH 17/17] feat: add ability to disable the cat language
 conversion

---
 docs/api-change.md                            |  4 ++
 docs/changelog.md                             |  1 +
 docs/downgrade.sql                            |  4 ++
 locales/en-US.yml                             |  1 +
 packages/backend-rs/index.d.ts                |  3 ++
 packages/backend-rs/index.js                  |  1 +
 packages/backend-rs/src/database/cache.rs     |  2 +
 packages/backend-rs/src/misc/mod.rs           |  1 +
 packages/backend-rs/src/misc/should_nyaify.rs | 39 +++++++++++++++++++
 packages/backend-rs/src/model/entity/user.rs  |  2 +
 .../1720107645050-turn-off-cat-language.ts    | 16 ++++++++
 packages/backend/src/models/entities/user.ts  |  6 +++
 .../backend/src/models/repositories/note.ts   | 15 ++++++-
 .../backend/src/models/repositories/user.ts   |  3 +-
 .../src/server/api/endpoints/i/update.ts      |  3 ++
 .../client/src/pages/settings/general.vue     | 12 ++++++
 packages/firefish-js/src/api.types.ts         |  2 +
 packages/firefish-js/src/entities.ts          |  1 +
 packages/firefish-js/src/schema/user.ts       |  5 +++
 19 files changed, 118 insertions(+), 3 deletions(-)
 create mode 100644 packages/backend-rs/src/misc/should_nyaify.rs
 create mode 100644 packages/backend/src/migration/1720107645050-turn-off-cat-language.ts

diff --git a/docs/api-change.md b/docs/api-change.md
index 2ae2275707..4147316c87 100644
--- a/docs/api-change.md
+++ b/docs/api-change.md
@@ -2,6 +2,10 @@
 
 Breaking changes are indicated by the :warning: icon.
 
+## Unreleased
+
+- Added `readCatLanguage` field to the response of `i` and request of `i/update` (optional).
+
 ## v20240607
 
 - `GET` request is now allowed for the `latest-version` endpoint.
diff --git a/docs/changelog.md b/docs/changelog.md
index 513370dd63..dc8873931d 100644
--- a/docs/changelog.md
+++ b/docs/changelog.md
@@ -7,6 +7,7 @@ Critical security updates are indicated by the :warning: icon.
 
 ## Unreleased
 
+- Add ability to disable the cat language conversion (nyaification)
 - Fix bugs
 
 ## [v20240630](https://firefish.dev/firefish/firefish/-/merge_requests/11072/commits)
diff --git a/docs/downgrade.sql b/docs/downgrade.sql
index 895046087a..b7a993beba 100644
--- a/docs/downgrade.sql
+++ b/docs/downgrade.sql
@@ -1,6 +1,7 @@
 BEGIN;
 
 DELETE FROM "migrations" WHERE name IN (
+    'TurnOffCatLanguage1720107645050',
     'RefactorScheduledPosts1716804636187',
     'RemoveEnumTypenameSuffix1716462794927',
     'CreateScheduledNote1714728200194',
@@ -34,6 +35,9 @@ DELETE FROM "migrations" WHERE name IN (
     'RemoveNativeUtilsMigration1705877093218'
 );
 
+-- turn-off-cat-language
+ALTER TABLE "user" DROP COLUMN "readCatLanguage";
+
 -- refactor-scheduled-post
 CREATE TABLE "scheduled_note" (
 	"id" character varying(32) NOT NULL PRIMARY KEY,
diff --git a/locales/en-US.yml b/locales/en-US.yml
index 261b0d5673..3a09448ac1 100644
--- a/locales/en-US.yml
+++ b/locales/en-US.yml
@@ -1240,6 +1240,7 @@ showNoAltTextWarning: "Show a warning if you attempt to post files without a des
 showAddFileDescriptionAtFirstPost: "Automatically open a form to write a description
   when you attempt to post files without a description"
 addAlt4MeTag: "Automatically append #Alt4Me hashtag to your post if attached file has no description"
+turnOffCatLanguage: "Turn off cat language conversion"
 
 _emojiModPerm:
   unauthorized: "None"
diff --git a/packages/backend-rs/index.d.ts b/packages/backend-rs/index.d.ts
index 52e4e1af1a..6967d64bf0 100644
--- a/packages/backend-rs/index.d.ts
+++ b/packages/backend-rs/index.d.ts
@@ -1295,6 +1295,8 @@ export interface Services {
   outbound: Array<Outbound>
 }
 
+export declare function shouldNyaify(readerUserId: string): Promise<boolean>
+
 /** Prints the server hardware information as the server info log. */
 export declare function showServerInfo(): void
 
@@ -1416,6 +1418,7 @@ export interface User {
   emojiModPerm: UserEmojiModPerm
   isIndexable: boolean
   alsoKnownAs: Array<string> | null
+  readCatLanguage: boolean
 }
 
 export const USER_ACTIVE_THRESHOLD: number
diff --git a/packages/backend-rs/index.js b/packages/backend-rs/index.js
index 9cb1779766..7edce9047d 100644
--- a/packages/backend-rs/index.js
+++ b/packages/backend-rs/index.js
@@ -432,6 +432,7 @@ module.exports.removeOldAttestationChallenges = nativeBinding.removeOldAttestati
 module.exports.safeForSql = nativeBinding.safeForSql
 module.exports.SECOND = nativeBinding.SECOND
 module.exports.sendPushNotification = nativeBinding.sendPushNotification
+module.exports.shouldNyaify = nativeBinding.shouldNyaify
 module.exports.showServerInfo = nativeBinding.showServerInfo
 module.exports.sqlLikeEscape = nativeBinding.sqlLikeEscape
 module.exports.storageUsage = nativeBinding.storageUsage
diff --git a/packages/backend-rs/src/database/cache.rs b/packages/backend-rs/src/database/cache.rs
index 2fe192cb1e..60abea56e8 100644
--- a/packages/backend-rs/src/database/cache.rs
+++ b/packages/backend-rs/src/database/cache.rs
@@ -9,6 +9,7 @@ pub enum Category {
     FetchUrl,
     Block,
     Follow,
+    CatLang,
     #[cfg(test)]
     Test,
 }
@@ -33,6 +34,7 @@ fn categorize(category: Category, key: &str) -> String {
         Category::FetchUrl => "fetchUrl",
         Category::Block => "blocking",
         Category::Follow => "following",
+        Category::CatLang => "catlang",
         #[cfg(test)]
         Category::Test => "usedOnlyForTesting",
     };
diff --git a/packages/backend-rs/src/misc/mod.rs b/packages/backend-rs/src/misc/mod.rs
index e3eb23a2eb..ca08b14f86 100644
--- a/packages/backend-rs/src/misc/mod.rs
+++ b/packages/backend-rs/src/misc/mod.rs
@@ -16,4 +16,5 @@ pub mod nyaify;
 pub mod password;
 pub mod reaction;
 pub mod remove_old_attestation_challenges;
+pub mod should_nyaify;
 pub mod system_info;
diff --git a/packages/backend-rs/src/misc/should_nyaify.rs b/packages/backend-rs/src/misc/should_nyaify.rs
new file mode 100644
index 0000000000..93ed5a1e7e
--- /dev/null
+++ b/packages/backend-rs/src/misc/should_nyaify.rs
@@ -0,0 +1,39 @@
+//! Determine whether to enable the cat language conversion
+
+use crate::{
+    database::{cache, db_conn},
+    model::entity::user,
+};
+use sea_orm::{DbErr, EntityTrait, QuerySelect, SelectColumns};
+
+#[derive(thiserror::Error, Debug)]
+pub enum Error {
+    #[doc = "database error"]
+    #[error(transparent)]
+    Db(#[from] DbErr),
+    #[doc = "cache error"]
+    #[error(transparent)]
+    Cache(#[from] cache::Error),
+    #[error("user {0} not found")]
+    NotFound(String),
+}
+
+#[macros::export]
+pub async fn should_nyaify(reader_user_id: &str) -> Result<bool, Error> {
+    let cached_value = cache::get_one::<bool>(cache::Category::CatLang, reader_user_id).await?;
+    if let Some(value) = cached_value {
+        return Ok(value);
+    }
+
+    let fetched_value = user::Entity::find_by_id(reader_user_id)
+        .select_only()
+        .select_column(user::Column::ReadCatLanguage)
+        .into_tuple::<bool>()
+        .one(db_conn().await?)
+        .await?
+        .ok_or_else(|| Error::NotFound(reader_user_id.to_owned()))?;
+
+    cache::set_one(cache::Category::CatLang, reader_user_id, &fetched_value, 10 * 60).await?;
+
+    Ok(fetched_value)
+}
diff --git a/packages/backend-rs/src/model/entity/user.rs b/packages/backend-rs/src/model/entity/user.rs
index f11e1e43d9..f13dc4c9af 100644
--- a/packages/backend-rs/src/model/entity/user.rs
+++ b/packages/backend-rs/src/model/entity/user.rs
@@ -77,6 +77,8 @@ pub struct Model {
     pub is_indexable: bool,
     #[sea_orm(column_name = "alsoKnownAs")]
     pub also_known_as: Option<Vec<String>>,
+    #[sea_orm(column_name = "readCatLanguage")]
+    pub read_cat_language: bool,
 }
 
 #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
diff --git a/packages/backend/src/migration/1720107645050-turn-off-cat-language.ts b/packages/backend/src/migration/1720107645050-turn-off-cat-language.ts
new file mode 100644
index 0000000000..8503ce96e9
--- /dev/null
+++ b/packages/backend/src/migration/1720107645050-turn-off-cat-language.ts
@@ -0,0 +1,16 @@
+import type { MigrationInterface, QueryRunner } from "typeorm";
+
+export class TurnOffCatLanguage1720107645050 implements MigrationInterface {
+	public async up(queryRunner: QueryRunner): Promise<void> {
+		await queryRunner.query(
+			`ALTER TABLE "user" ADD COLUMN "readCatLanguage" boolean NOT NULL DEFAULT true`,
+		);
+		await queryRunner.query(
+			`COMMENT ON COLUMN "user"."readCatLanguage" IS 'Whether to enable the cat language conversion.'`,
+		);
+	}
+
+	public async down(queryRunner: QueryRunner): Promise<void> {
+		await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "readCatLanguage"`);
+	}
+}
diff --git a/packages/backend/src/models/entities/user.ts b/packages/backend/src/models/entities/user.ts
index 65780eb44d..e71e04ef8d 100644
--- a/packages/backend/src/models/entities/user.ts
+++ b/packages/backend/src/models/entities/user.ts
@@ -159,6 +159,12 @@ export class User {
 	})
 	public speakAsCat: boolean;
 
+	@Column("boolean", {
+		default: true,
+		comment: "Whether to enable the cat language conversion.",
+	})
+	public readCatLanguage: boolean;
+
 	@Column("boolean", {
 		default: false,
 		comment: "Whether the User is the admin.",
diff --git a/packages/backend/src/models/repositories/note.ts b/packages/backend/src/models/repositories/note.ts
index 062973a741..3a24c0b80d 100644
--- a/packages/backend/src/models/repositories/note.ts
+++ b/packages/backend/src/models/repositories/note.ts
@@ -13,7 +13,12 @@ import {
 	Notes,
 } from "../index.js";
 import type { Packed } from "@/misc/schema.js";
-import { countReactions, decodeReaction, nyaify } from "backend-rs";
+import {
+	countReactions,
+	decodeReaction,
+	nyaify,
+	shouldNyaify,
+} from "backend-rs";
 import { awaitAll } from "@/prelude/await-all.js";
 import type { NoteReaction } from "@/models/entities/note-reaction.js";
 import {
@@ -285,7 +290,13 @@ export const NoteRepository = db.getRepository(Note).extend({
 			lang: note.lang,
 		});
 
-		if (packed.user.isCat && packed.user.speakAsCat && packed.text) {
+		if (
+			packed.user.isCat &&
+			packed.user.speakAsCat &&
+			packed.text != null &&
+			meId != null &&
+			(await shouldNyaify(meId))
+		) {
 			const tokens = packed.text ? mfm.parse(packed.text) : [];
 			function nyaifyNode(node: mfm.MfmNode) {
 				if (node.type === "quote") return;
diff --git a/packages/backend/src/models/repositories/user.ts b/packages/backend/src/models/repositories/user.ts
index c07473d0ea..e6c8f2517c 100644
--- a/packages/backend/src/models/repositories/user.ts
+++ b/packages/backend/src/models/repositories/user.ts
@@ -464,7 +464,8 @@ export const UserRepository = db.getRepository(User).extend({
 			isLocked: user.isLocked,
 			isIndexable: user.isIndexable,
 			isCat: user.isCat || falsy,
-			speakAsCat: user.speakAsCat || falsy,
+			speakAsCat: user.speakAsCat,
+			readCatLanguage: user.readCatLanguage,
 			instance: user.host
 				? userInstanceCache
 						.fetch(
diff --git a/packages/backend/src/server/api/endpoints/i/update.ts b/packages/backend/src/server/api/endpoints/i/update.ts
index 98d760ddb5..dd0511cfed 100644
--- a/packages/backend/src/server/api/endpoints/i/update.ts
+++ b/packages/backend/src/server/api/endpoints/i/update.ts
@@ -114,6 +114,7 @@ export const paramDef = {
 		isBot: { type: "boolean" },
 		isCat: { type: "boolean" },
 		speakAsCat: { type: "boolean", nullable: true },
+		readCatLanguage: { type: "boolean", nullable: true },
 		isIndexable: { type: "boolean" },
 		injectFeaturedNote: { type: "boolean" },
 		receiveAnnouncementEmail: { type: "boolean" },
@@ -220,6 +221,8 @@ export default define(meta, paramDef, async (ps, _user, token) => {
 		profileUpdates.isIndexable = ps.isIndexable;
 	}
 	if (typeof ps.speakAsCat === "boolean") updates.speakAsCat = ps.speakAsCat;
+	if (typeof ps.readCatLanguage === "boolean")
+		updates.readCatLanguage = ps.readCatLanguage;
 	if (typeof ps.injectFeaturedNote === "boolean")
 		profileUpdates.injectFeaturedNote = ps.injectFeaturedNote;
 	if (typeof ps.receiveAnnouncementEmail === "boolean")
diff --git a/packages/client/src/pages/settings/general.vue b/packages/client/src/pages/settings/general.vue
index 54b9f4aa62..aa6d2a9ad5 100644
--- a/packages/client/src/pages/settings/general.vue
+++ b/packages/client/src/pages/settings/general.vue
@@ -200,6 +200,11 @@
 					i18n.ts.expandOnNoteClickDesc
 				}}</template>
 			</FormSwitch>
+			<FormSwitch v-model="turnOffCatLanguage" @update:modelValue="save()" class="_formBlock"
+				>{{ i18n.ts.turnOffCatLanguage }}<template #caption>{{
+					i18n.ts.reflectMayTakeTime
+				}}</template>
+			</FormSwitch>
 			<FormSwitch v-model="advancedMfm" class="_formBlock">
 				{{ i18n.ts._mfm.advanced
 				}}<template #caption>{{
@@ -423,6 +428,13 @@ const serverLang = ref(me?.lang);
 const translateLang = ref(localStorage.getItem("translateLang"));
 const fontSize = ref(localStorage.getItem("fontSize"));
 const useSystemFont = ref(localStorage.getItem("useSystemFont") !== "f");
+const turnOffCatLanguage = ref(!me?.readCatLanguage);
+
+function save() {
+	os.api("i/update", {
+		readCatLanguage: !turnOffCatLanguage.value,
+	});
+}
 
 async function reloadAsk() {
 	const { canceled } = await os.confirm({
diff --git a/packages/firefish-js/src/api.types.ts b/packages/firefish-js/src/api.types.ts
index 140667741f..58ef766991 100644
--- a/packages/firefish-js/src/api.types.ts
+++ b/packages/firefish-js/src/api.types.ts
@@ -598,6 +598,8 @@ export type Endpoints = {
 			preventAiLearning?: boolean;
 			isBot?: boolean;
 			isCat?: boolean;
+			speakAsCat?: boolean;
+			readCatLanguage?: boolean;
 			injectFeaturedNote?: boolean;
 			receiveAnnouncementEmail?: boolean;
 			alwaysMarkNsfw?: boolean;
diff --git a/packages/firefish-js/src/entities.ts b/packages/firefish-js/src/entities.ts
index 2541b255c9..83ebc2514c 100644
--- a/packages/firefish-js/src/entities.ts
+++ b/packages/firefish-js/src/entities.ts
@@ -30,6 +30,7 @@ export type UserLite = {
 	isIndexable: boolean;
 	isCat?: boolean;
 	speakAsCat?: boolean;
+	readCatLanguage?: boolean;
 	driveCapacityOverrideMb: number | null;
 };
 
diff --git a/packages/firefish-js/src/schema/user.ts b/packages/firefish-js/src/schema/user.ts
index c7417ce0e4..702a8b380a 100644
--- a/packages/firefish-js/src/schema/user.ts
+++ b/packages/firefish-js/src/schema/user.ts
@@ -76,6 +76,11 @@ export const packedUserLiteSchema = {
 			nullable: false,
 			optional: true,
 		},
+		readCatLanguage: {
+			type: "boolean",
+			nullable: false,
+			optional: true,
+		},
 		emojis: {
 			type: "array",
 			nullable: false,