diff --git a/locales/en-US.yml b/locales/en-US.yml
index 8802039220..3649e97011 100644
--- a/locales/en-US.yml
+++ b/locales/en-US.yml
@@ -146,6 +146,8 @@ flagAsBot: "Mark this account as a bot"
flagAsBotDescription: "Enable this option if this account is controlled by a program. If enabled, it will act as a flag for other developers to prevent endless interaction chains with other bots and adjust Calckey's internal systems to treat this account as a bot."
flagAsCat: "Are you a cat? 😺"
flagAsCatDescription: "You'll get cat ears and speak like a cat!"
+flagSpeakAsCat: "Speak as a cat"
+flagSpeakAsCatDescription: "Your posts will get nyanified when in cat mode"
flagShowTimelineReplies: "Show replies in timeline"
flagShowTimelineRepliesDescription: "Shows replies of users to posts of other users in the timeline if turned on."
autoAcceptFollowed: "Automatically approve follow requests from users you're following"
diff --git a/packages/backend/migration/1680426269172-SpeakAsCat.js b/packages/backend/migration/1680426269172-SpeakAsCat.js
new file mode 100644
index 0000000000..3655e27265
--- /dev/null
+++ b/packages/backend/migration/1680426269172-SpeakAsCat.js
@@ -0,0 +1,20 @@
+export class SpeakAsCat1680426269172 {
+ name = 'SpeakAsCat1680426269172'
+
+ async up(queryRunner) {
+ await queryRunner.query(`
+ ALTER TABLE "user"
+ ADD "speakAsCat" boolean NOT NULL DEFAULT true
+ `);
+ await queryRunner.query(`
+ COMMENT ON COLUMN "user"."speakAsCat"
+ IS 'Whether to speak as a cat if isCat.'
+ `);
+ }
+
+ async down(queryRunner) {
+ await queryRunner.query(`
+ ALTER TABLE "user" DROP COLUMN "speakAsCat"
+ `);
+ }
+}
diff --git a/packages/backend/src/models/entities/user.ts b/packages/backend/src/models/entities/user.ts
index c57ad916c9..c23f4f28d7 100644
--- a/packages/backend/src/models/entities/user.ts
+++ b/packages/backend/src/models/entities/user.ts
@@ -156,6 +156,12 @@ export class User {
})
public isCat: boolean;
+ @Column('boolean', {
+ default: true,
+ comment: 'Whether to speak as a cat if isCat.',
+ })
+ public speakAsCat: 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 b99ce16350..5e56a817bc 100644
--- a/packages/backend/src/models/repositories/note.ts
+++ b/packages/backend/src/models/repositories/note.ts
@@ -263,7 +263,7 @@ export const NoteRepository = db.getRepository(Note).extend({
: {}),
});
- if (packed.user.isCat && packed.text) {
+ if (packed.user.isCat && packed.user.speakAsCat && packed.text) {
const tokens = packed.text ? mfm.parse(packed.text) : [];
function nyaizeNode(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 0bf31b1b33..27b0c78d6e 100644
--- a/packages/backend/src/models/repositories/user.ts
+++ b/packages/backend/src/models/repositories/user.ts
@@ -438,6 +438,7 @@ export const UserRepository = db.getRepository(User).extend({
isModerator: user.isModerator || falsy,
isBot: user.isBot || falsy,
isCat: user.isCat || falsy,
+ speakAsCat: user.speakAsCat || falsy,
instance: user.host
? userInstanceCache
.fetch(
diff --git a/packages/backend/src/models/schema/user.ts b/packages/backend/src/models/schema/user.ts
index 9fb5aa8f3b..7f76891650 100644
--- a/packages/backend/src/models/schema/user.ts
+++ b/packages/backend/src/models/schema/user.ts
@@ -66,6 +66,11 @@ export const packedUserLiteSchema = {
nullable: false,
optional: true,
},
+ speakAsCat: {
+ type: "boolean",
+ nullable: false,
+ optional: true,
+ },
emojis: {
type: "array",
nullable: false,
diff --git a/packages/backend/src/server/api/endpoints/i/update.ts b/packages/backend/src/server/api/endpoints/i/update.ts
index 48868de37e..56ed64296b 100644
--- a/packages/backend/src/server/api/endpoints/i/update.ts
+++ b/packages/backend/src/server/api/endpoints/i/update.ts
@@ -104,6 +104,7 @@ export const paramDef = {
noCrawle: { type: "boolean" },
isBot: { type: "boolean" },
isCat: { type: "boolean" },
+ speakAsCat: { type: "boolean" },
showTimelineReplies: { type: "boolean" },
injectFeaturedNote: { type: "boolean" },
receiveAnnouncementEmail: { type: "boolean" },
@@ -191,6 +192,7 @@ export default define(meta, paramDef, async (ps, _user, token) => {
profileUpdates.autoAcceptFollowed = ps.autoAcceptFollowed;
if (typeof ps.noCrawle === "boolean") profileUpdates.noCrawle = ps.noCrawle;
if (typeof ps.isCat === "boolean") updates.isCat = ps.isCat;
+ if (typeof ps.speakAsCat === "boolean") updates.speakAsCat = ps.speakAsCat;
if (typeof ps.injectFeaturedNote === "boolean")
profileUpdates.injectFeaturedNote = ps.injectFeaturedNote;
if (typeof ps.receiveAnnouncementEmail === "boolean")
diff --git a/packages/client/src/pages/settings/profile.vue b/packages/client/src/pages/settings/profile.vue
index a2ed6fd41f..4e51a339b7 100644
--- a/packages/client/src/pages/settings/profile.vue
+++ b/packages/client/src/pages/settings/profile.vue
@@ -59,6 +59,7 @@