diff --git a/CHANGELOG.md b/CHANGELOG.md index d2cdde81d..961e5fcca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -57,6 +57,7 @@ - カスタム絵文字のライセンスを複数でセットできるようになりました。 - 管理者が予約ユーザー名を設定できるようになりました。 - Fix: フォローリクエストの通知が残る問題を修正 +- センシティブワードの登録にAnd、正規表現が使用できるようになりました。 ### Client - アカウント作成時に初期設定ウィザードを表示するように diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index cfad31348..4f458dc4e 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -990,6 +990,7 @@ rolesAssignedToMe: "自分に割り当てられたロール" resetPasswordConfirm: "パスワードリセットしますか?" sensitiveWords: "センシティブワード" sensitiveWordsDescription: "設定したワードが含まれるノートの公開範囲をホームにします。改行で区切って複数設定できます。" +sensitiveWordsDescription2: "スペースで区切るとAND指定になり、キーワードをスラッシュで囲むと正規表現になります。" notesSearchNotAvailable: "ノート検索は利用できません。" license: "ライセンス" unfavoriteConfirm: "お気に入り解除しますか?" diff --git a/packages/backend/src/core/NoteCreateService.ts b/packages/backend/src/core/NoteCreateService.ts index 364976e4a..977c9052c 100644 --- a/packages/backend/src/core/NoteCreateService.ts +++ b/packages/backend/src/core/NoteCreateService.ts @@ -3,6 +3,7 @@ import * as mfm from 'mfm-js'; import { In, DataSource } from 'typeorm'; import * as Redis from 'ioredis'; import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common'; +import RE2 from 're2'; import { extractMentions } from '@/misc/extract-mentions.js'; import { extractCustomEmojisFromMfm } from '@/misc/extract-custom-emojis-from-mfm.js'; import { extractHashtags } from '@/misc/extract-hashtags.js'; @@ -238,7 +239,8 @@ export class NoteCreateService implements OnApplicationShutdown { if (data.channel != null) data.localOnly = true; if (data.visibility === 'public' && data.channel == null) { - if ((data.text != null) && (await this.metaService.fetch()).sensitiveWords.some(w => data.text!.includes(w))) { + const sensitiveWords = (await this.metaService.fetch()).sensitiveWords; + if (this.isSensitive(data, sensitiveWords)) { data.visibility = 'home'; } else if ((await this.roleService.getUserPolicies(user.id)).canPublicNote === false) { data.visibility = 'home'; @@ -670,6 +672,31 @@ export class NoteCreateService implements OnApplicationShutdown { // Register to search database this.index(note); } + + @bindThis + private isSensitive(note: Option, sensitiveWord: string[]): boolean { + if (sensitiveWord.length > 0) { + const text = note.cw ?? note.text ?? ''; + if (text === '') return false; + const matched = sensitiveWord.some(filter => { + // represents RegExp + const regexp = filter.match(/^\/(.+)\/(.*)$/); + // This should never happen due to input sanitisation. + if (!regexp) { + const words = filter.split(' '); + return words.every(keyword => text.includes(keyword)); + } + try { + return new RE2(regexp[1], regexp[2]).test(text); + } catch (err) { + // This should never happen due to input sanitisation. + return false; + } + }); + if (matched) return true; + } + return false; + } @bindThis private incRenoteCount(renote: Note) { diff --git a/packages/backend/test/e2e/note.ts b/packages/backend/test/e2e/note.ts index 9c851a5dd..d2eb8f01d 100644 --- a/packages/backend/test/e2e/note.ts +++ b/packages/backend/test/e2e/note.ts @@ -541,6 +541,61 @@ describe('Note', () => { assert.strictEqual(res.status, 400); }); + + test('センシティブな投稿はhomeになる (単語指定)', async () => { + const sensitive = await api('admin/update-meta', { + sensitiveWords: [ + "test", + ] + }, alice); + + assert.strictEqual(sensitive.status, 204); + + await new Promise(x => setTimeout(x, 2)); + + const note1 = await api('/notes/create', { + text: 'hogetesthuge', + }, alice); + + assert.strictEqual(note1.status, 200); + assert.strictEqual(note1.body.createdNote.visibility, 'home'); + + }); + + test('センシティブな投稿はhomeになる (正規表現)', async () => { + const sensitive = await api('admin/update-meta', { + sensitiveWords: [ + "/Test/i", + ] + }, alice); + + assert.strictEqual(sensitive.status, 204); + + const note2 = await api('/notes/create', { + text: 'hogetesthuge', + }, alice); + + assert.strictEqual(note2.status, 200); + assert.strictEqual(note2.body.createdNote.visibility, 'home'); + }); + + test('センシティブな投稿はhomeになる (スペースアンド)', async () => { + const sensitive = await api('admin/update-meta', { + sensitiveWords: [ + "Test hoge" + ] + }, alice); + + assert.strictEqual(sensitive.status, 204); + + const note2 = await api('/notes/create', { + text: 'hogeTesthuge', + }, alice); + + assert.strictEqual(note2.status, 200); + assert.strictEqual(note2.body.createdNote.visibility, 'home'); + + }); }); describe('notes/delete', () => { diff --git a/packages/frontend/src/pages/admin/moderation.vue b/packages/frontend/src/pages/admin/moderation.vue index ffd3b6e23..bf788e360 100644 --- a/packages/frontend/src/pages/admin/moderation.vue +++ b/packages/frontend/src/pages/admin/moderation.vue @@ -27,7 +27,7 @@ - +