From 7f16b50e732183a9ba8995d1ca66049512963d10 Mon Sep 17 00:00:00 2001 From: syuilo Date: Mon, 13 Mar 2023 17:37:22 +0900 Subject: [PATCH] feat: sensitive word --- CHANGELOG.md | 1 + locales/ja-JP.yml | 2 + .../1678694614599-sensitive-words.js | 11 +++ .../backend/src/core/NoteCreateService.ts | 6 +- packages/backend/src/models/entities/Meta.ts | 5 ++ .../src/server/api/endpoints/admin/meta.ts | 9 +++ .../server/api/endpoints/admin/update-meta.ts | 7 ++ packages/frontend/src/pages/admin/index.vue | 5 ++ .../frontend/src/pages/admin/moderation.vue | 73 +++++++++++++++++++ packages/frontend/src/router.ts | 4 + 10 files changed, 122 insertions(+), 1 deletion(-) create mode 100644 packages/backend/migration/1678694614599-sensitive-words.js create mode 100644 packages/frontend/src/pages/admin/moderation.vue diff --git a/CHANGELOG.md b/CHANGELOG.md index 987328a970..22e09b2cac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ You should also include the user name that made the change. - ユーザーごとにRenoteをミュートできるように - ノートごとに絵文字リアクションを受け取るか設定できるように - ロールの並び順を設定可能に +- 指定した文字列を含む投稿の公開範囲をホームにできるように - enhance(client): 設定から自分のロールを確認できるように - enhance(client): DM作成時にメンションも含むように - enhance(client): フォロー申請のボタンのデザインを改善 diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index efe9306e59..992595734f 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -971,6 +971,8 @@ likeOnly: "いいねのみ" likeOnlyForRemote: "リモートからはいいねのみ" rolesAssignedToMe: "自分に割り当てられたロール" resetPasswordConfirm: "パスワードリセットしますか?" +sensitiveWords: "センシティブワード" +sensitiveWordsDescription: "設定したワードが含まれるノートの公開範囲をホームにします。改行で区切って複数設定できます。" _achievements: earnedAt: "獲得日時" diff --git a/packages/backend/migration/1678694614599-sensitive-words.js b/packages/backend/migration/1678694614599-sensitive-words.js new file mode 100644 index 0000000000..6d4c5730c7 --- /dev/null +++ b/packages/backend/migration/1678694614599-sensitive-words.js @@ -0,0 +1,11 @@ +export class sensitiveWords1678694614599 { + name = 'sensitiveWords1678694614599' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "meta" ADD "sensitiveWords" character varying(1024) array NOT NULL DEFAULT '{}'`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "sensitiveWords"`); + } +} diff --git a/packages/backend/src/core/NoteCreateService.ts b/packages/backend/src/core/NoteCreateService.ts index 8d8535ca5b..5a4df69b62 100644 --- a/packages/backend/src/core/NoteCreateService.ts +++ b/packages/backend/src/core/NoteCreateService.ts @@ -44,6 +44,7 @@ import { RemoteUserResolveService } from '@/core/RemoteUserResolveService.js'; import { bindThis } from '@/decorators.js'; import { DB_MAX_NOTE_TEXT_LENGTH } from '@/const.js'; import { RoleService } from '@/core/RoleService.js'; +import { MetaService } from '@/core/MetaService.js'; const mutedWordsCache = new Cache<{ userId: UserProfile['userId']; mutedWords: UserProfile['mutedWords']; }[]>(1000 * 60 * 5); @@ -192,6 +193,7 @@ export class NoteCreateService implements OnApplicationShutdown { private apDeliverManagerService: ApDeliverManagerService, private apRendererService: ApRendererService, private roleService: RoleService, + private metaService: MetaService, private notesChart: NotesChart, private perUserNotesChart: PerUserNotesChart, private activeUsersChart: ActiveUsersChart, @@ -230,7 +232,9 @@ export class NoteCreateService implements OnApplicationShutdown { if (data.channel != null) data.localOnly = true; if (data.visibility === 'public' && data.channel == null) { - if ((await this.roleService.getUserPolicies(user.id)).canPublicNote === false) { + if ((data.text != null) && (await this.metaService.fetch()).sensitiveWords.some(w => data.text!.includes(w))) { + data.visibility = 'home'; + } else if ((await this.roleService.getUserPolicies(user.id)).canPublicNote === false) { data.visibility = 'home'; } } diff --git a/packages/backend/src/models/entities/Meta.ts b/packages/backend/src/models/entities/Meta.ts index 741065da2d..57338ecbd2 100644 --- a/packages/backend/src/models/entities/Meta.ts +++ b/packages/backend/src/models/entities/Meta.ts @@ -67,6 +67,11 @@ export class Meta { }) public blockedHosts: string[]; + @Column('varchar', { + length: 1024, array: true, default: '{}', + }) + public sensitiveWords: string[]; + @Column('varchar', { length: 1024, nullable: true, diff --git a/packages/backend/src/server/api/endpoints/admin/meta.ts b/packages/backend/src/server/api/endpoints/admin/meta.ts index 224d37e7a7..ce7e0d569d 100644 --- a/packages/backend/src/server/api/endpoints/admin/meta.ts +++ b/packages/backend/src/server/api/endpoints/admin/meta.ts @@ -110,6 +110,14 @@ export const meta = { optional: false, nullable: false, }, }, + sensitiveWords: { + type: 'array', + optional: true, nullable: false, + items: { + type: 'string', + optional: false, nullable: false, + }, + }, hcaptchaSecretKey: { type: 'string', optional: true, nullable: true, @@ -295,6 +303,7 @@ export default class extends Endpoint { pinnedUsers: instance.pinnedUsers, hiddenTags: instance.hiddenTags, blockedHosts: instance.blockedHosts, + sensitiveWords: instance.sensitiveWords, hcaptchaSecretKey: instance.hcaptchaSecretKey, recaptchaSecretKey: instance.recaptchaSecretKey, turnstileSecretKey: instance.turnstileSecretKey, diff --git a/packages/backend/src/server/api/endpoints/admin/update-meta.ts b/packages/backend/src/server/api/endpoints/admin/update-meta.ts index 753682406d..2f23aca243 100644 --- a/packages/backend/src/server/api/endpoints/admin/update-meta.ts +++ b/packages/backend/src/server/api/endpoints/admin/update-meta.ts @@ -27,6 +27,9 @@ export const paramDef = { blockedHosts: { type: 'array', nullable: true, items: { type: 'string', } }, + sensitiveWords: { type: 'array', nullable: true, items: { + type: 'string', + } }, themeColor: { type: 'string', nullable: true, pattern: '^#[0-9a-fA-F]{6}$' }, mascotImageUrl: { type: 'string', nullable: true }, bannerUrl: { type: 'string', nullable: true }, @@ -127,6 +130,10 @@ export default class extends Endpoint { set.blockedHosts = ps.blockedHosts.filter(Boolean).map(x => x.toLowerCase()); } + if (Array.isArray(ps.sensitiveWords)) { + set.sensitiveWords = ps.sensitiveWords.filter(Boolean); + } + if (ps.themeColor !== undefined) { set.themeColor = ps.themeColor; } diff --git a/packages/frontend/src/pages/admin/index.vue b/packages/frontend/src/pages/admin/index.vue index 5ebbe7e97b..550de24bb2 100644 --- a/packages/frontend/src/pages/admin/index.vue +++ b/packages/frontend/src/pages/admin/index.vue @@ -143,6 +143,11 @@ const menuDef = $computed(() => [{ text: i18n.ts.general, to: '/admin/settings', active: currentPage?.route.name === 'settings', + }, { + icon: 'ti ti-shield', + text: i18n.ts.moderation, + to: '/admin/moderation', + active: currentPage?.route.name === 'moderation', }, { icon: 'ti ti-mail', text: i18n.ts.emailServer, diff --git a/packages/frontend/src/pages/admin/moderation.vue b/packages/frontend/src/pages/admin/moderation.vue new file mode 100644 index 0000000000..7c2f04a9ab --- /dev/null +++ b/packages/frontend/src/pages/admin/moderation.vue @@ -0,0 +1,73 @@ + + + + + diff --git a/packages/frontend/src/router.ts b/packages/frontend/src/router.ts index c51c92bf06..5f184881b4 100644 --- a/packages/frontend/src/router.ts +++ b/packages/frontend/src/router.ts @@ -384,6 +384,10 @@ export const routes = [{ path: '/settings', name: 'settings', component: page(() => import('./pages/admin/settings.vue')), + }, { + path: '/moderation', + name: 'moderation', + component: page(() => import('./pages/admin/moderation.vue')), }, { path: '/email-settings', name: 'email-settings',