From d1807ee5dc9d7677cf60ad5139b490b6307807ce Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Sat, 14 Jan 2023 08:04:38 +0900
Subject: [PATCH] =?UTF-8?q?enhance:=20=E3=83=8F=E3=83=BC=E3=83=89=E3=83=AF?=
 =?UTF-8?q?=E3=83=BC=E3=83=89=E3=83=9F=E3=83=A5=E3=83=BC=E3=83=88=E3=81=AE?=
 =?UTF-8?q?=E6=9C=80=E5=A4=A7=E6=96=87=E5=AD=97=E6=95=B0=E3=82=92=E8=A8=AD?=
 =?UTF-8?q?=E5=AE=9A=E5=8F=AF=E8=83=BD=E3=81=AB?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Resolve #7574
---
 CHANGELOG.md                                     |  5 +++--
 locales/ja-JP.yml                                |  1 +
 packages/backend/src/core/RoleService.ts         |  3 +++
 .../backend/src/server/api/endpoints/i/update.ts | 14 ++++++++++++++
 .../frontend/src/pages/admin/roles.editor.vue    | 16 ++++++++++++++++
 packages/frontend/src/pages/admin/roles.vue      | 11 +++++++++++
 6 files changed, 48 insertions(+), 2 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index ac9d4b835a..7ca44f87f9 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -73,8 +73,9 @@ You should also include the user name that made the change.
 - Push notification of Antenna note @tamaina
 - AVIF support @tamaina
 - Add Cloudflare Turnstile CAPTCHA support @CyberRex0
-- 非モデレーターでも、権限を持つロールをアサインされたユーザーはインスタンスの招待コードを発行できるように
-- 非モデレーターでも、権限を持つロールをアサインされたユーザーはカスタム絵文字の追加、編集、削除を行えるように
+- 非モデレーターでも、権限を持つロールをアサインされたユーザーはインスタンスの招待コードを発行できるように @syuilo
+- 非モデレーターでも、権限を持つロールをアサインされたユーザーはカスタム絵文字の追加、編集、削除を行えるように @syuilo
+- ハードワードミュートの最大文字数を設定可能に @syuilo
 - Server: signToActivityPubGet is set to true by default @syuilo
 - Server: improve syslog performance @syuilo
 - Server: Use undici instead of node-fetch and got @tamaina
diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml
index daeacf7690..943168061c 100644
--- a/locales/ja-JP.yml
+++ b/locales/ja-JP.yml
@@ -962,6 +962,7 @@ _role:
     canManageCustomEmojis: "カスタム絵文字の管理"
     driveCapacity: "ドライブ容量"
     antennaMax: "アンテナの作成可能数"
+    wordMuteMax: "ワードミュートの最大文字数"
   _condition:
     isLocal: "ローカルユーザー"
     isRemote: "リモートユーザー"
diff --git a/packages/backend/src/core/RoleService.ts b/packages/backend/src/core/RoleService.ts
index 15ba900802..d2056709e1 100644
--- a/packages/backend/src/core/RoleService.ts
+++ b/packages/backend/src/core/RoleService.ts
@@ -20,6 +20,7 @@ export type RoleOptions = {
 	canManageCustomEmojis: boolean;
 	driveCapacityMb: number;
 	antennaLimit: number;
+	wordMuteLimit: number;
 };
 
 export const DEFAULT_ROLE: RoleOptions = {
@@ -30,6 +31,7 @@ export const DEFAULT_ROLE: RoleOptions = {
 	canManageCustomEmojis: false,
 	driveCapacityMb: 100,
 	antennaLimit: 5,
+	wordMuteLimit: 200,
 };
 
 @Injectable()
@@ -187,6 +189,7 @@ export class RoleService implements OnApplicationShutdown {
 			canManageCustomEmojis: getOptionValues('canManageCustomEmojis').some(x => x === true),
 			driveCapacityMb: Math.max(...getOptionValues('driveCapacityMb')),
 			antennaLimit: Math.max(...getOptionValues('antennaLimit')),
+			wordMuteLimit: Math.max(...getOptionValues('wordMuteLimit')),
 		};
 	}
 
diff --git a/packages/backend/src/server/api/endpoints/i/update.ts b/packages/backend/src/server/api/endpoints/i/update.ts
index c2231f5f75..ef5ab16bc4 100644
--- a/packages/backend/src/server/api/endpoints/i/update.ts
+++ b/packages/backend/src/server/api/endpoints/i/update.ts
@@ -17,6 +17,7 @@ import { UserFollowingService } from '@/core/UserFollowingService.js';
 import { AccountUpdateService } from '@/core/AccountUpdateService.js';
 import { HashtagService } from '@/core/HashtagService.js';
 import { DI } from '@/di-symbols.js';
+import { RoleService } from '@/core/RoleService.js';
 import { ApiError } from '../../error.js';
 
 export const meta = {
@@ -62,6 +63,12 @@ export const meta = {
 			code: 'INVALID_REGEXP',
 			id: '0d786918-10df-41cd-8f33-8dec7d9a89a5',
 		},
+
+		tooManyMutedWords: {
+			message: 'Too many muted words.',
+			code: 'TOO_MANY_MUTED_WORDS',
+			id: '010665b1-a211-42d2-bc64-8f6609d79785',
+		},
 	},
 
 	res: {
@@ -144,6 +151,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
 		private userFollowingService: UserFollowingService,
 		private accountUpdateService: AccountUpdateService,
 		private hashtagService: HashtagService,
+		private roleService: RoleService,
 	) {
 		super(meta, paramDef, async (ps, _user, token) => {
 			const user = await this.usersRepository.findOneByOrFail({ id: _user.id });
@@ -163,6 +171,12 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
 			if (ps.avatarId !== undefined) updates.avatarId = ps.avatarId;
 			if (ps.bannerId !== undefined) updates.bannerId = ps.bannerId;
 			if (ps.mutedWords !== undefined) {
+				// TODO: ちゃんと数える
+				const length = JSON.stringify(ps.mutedWords).length;
+				if (length > (await this.roleService.getUserRoleOptions(user.id)).antennaLimit) {
+					throw new ApiError(meta.errors.tooManyMutedWords);
+				}
+
 				// validate regular expression syntax
 				ps.mutedWords.filter(x => !Array.isArray(x)).forEach(x => {
 					const regexp = x.match(/^\/(.+)\/(.*)$/);
diff --git a/packages/frontend/src/pages/admin/roles.editor.vue b/packages/frontend/src/pages/admin/roles.editor.vue
index b66c967e6a..30bc6c238e 100644
--- a/packages/frontend/src/pages/admin/roles.editor.vue
+++ b/packages/frontend/src/pages/admin/roles.editor.vue
@@ -127,6 +127,19 @@
 					</MkInput>
 				</div>
 			</MkFolder>
+
+			<MkFolder>
+				<template #label>{{ i18n.ts._role._options.wordMuteMax }}</template>
+				<template #suffix>{{ options_wordMuteLimit_useDefault ? i18n.ts._role.useBaseValue : (options_wordMuteLimit_value) }}</template>
+				<div class="_gaps">
+					<MkSwitch v-model="options_wordMuteLimit_useDefault" :readonly="readonly">
+						<template #label>{{ i18n.ts._role.useBaseValue }}</template>
+					</MkSwitch>
+					<MkInput v-model="options_wordMuteLimit_value" :disabled="options_wordMuteLimit_useDefault" type="number" :readonly="readonly">
+						<template #suffix>chars</template>
+					</MkInput>
+				</div>
+			</MkFolder>
 		</div>
 	</FormSlot>
 
@@ -194,6 +207,8 @@ let options_driveCapacityMb_useDefault = $ref(role?.options?.driveCapacityMb?.us
 let options_driveCapacityMb_value = $ref(role?.options?.driveCapacityMb?.value ?? 0);
 let options_antennaLimit_useDefault = $ref(role?.options?.antennaLimit?.useDefault ?? true);
 let options_antennaLimit_value = $ref(role?.options?.antennaLimit?.value ?? 0);
+let options_wordMuteLimit_useDefault = $ref(role?.options?.wordMuteLimit?.useDefault ?? true);
+let options_wordMuteLimit_value = $ref(role?.options?.wordMuteLimit?.value ?? 0);
 
 if (_DEV_) {
 	watch($$(condFormula), () => {
@@ -210,6 +225,7 @@ function getOptions() {
 		canManageCustomEmojis: { useDefault: options_canManageCustomEmojis_useDefault, value: options_canManageCustomEmojis_value },
 		driveCapacityMb: { useDefault: options_driveCapacityMb_useDefault, value: options_driveCapacityMb_value },
 		antennaLimit: { useDefault: options_antennaLimit_useDefault, value: options_antennaLimit_value },
+		wordMuteLimit: { useDefault: options_wordMuteLimit_useDefault, value: options_wordMuteLimit_value },
 	};
 }
 
diff --git a/packages/frontend/src/pages/admin/roles.vue b/packages/frontend/src/pages/admin/roles.vue
index 16c12f9928..001800ea26 100644
--- a/packages/frontend/src/pages/admin/roles.vue
+++ b/packages/frontend/src/pages/admin/roles.vue
@@ -62,6 +62,15 @@
 							<MkInput v-model="options_antennaLimit" type="number">
 							</MkInput>
 						</MkFolder>
+
+						<MkFolder>
+							<template #label>{{ i18n.ts._role._options.wordMuteMax }}</template>
+							<template #suffix>{{ options_wordMuteLimit }}</template>
+							<MkInput v-model="options_wordMuteLimit" type="number">
+								<template #suffix>chars</template>
+							</MkInput>
+						</MkFolder>
+
 						<MkButton primary rounded @click="updateBaseRole">{{ i18n.ts.save }}</MkButton>
 					</div>
 				</MkFolder>
@@ -101,6 +110,7 @@ let options_canInvite = $ref(instance.baseRole.canInvite);
 let options_canManageCustomEmojis = $ref(instance.baseRole.canManageCustomEmojis);
 let options_driveCapacityMb = $ref(instance.baseRole.driveCapacityMb);
 let options_antennaLimit = $ref(instance.baseRole.antennaLimit);
+let options_wordMuteLimit = $ref(instance.baseRole.wordMuteLimit);
 
 async function updateBaseRole() {
 	await os.apiWithDialog('admin/roles/update-default-role-override', {
@@ -112,6 +122,7 @@ async function updateBaseRole() {
 			canManageCustomEmojis: options_canManageCustomEmojis,
 			driveCapacityMb: options_driveCapacityMb,
 			antennaLimit: options_antennaLimit,
+			wordMuteLimit: options_wordMuteLimit,
 		},
 	});
 }