summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGELOG.md2
-rw-r--r--locales/index.d.ts12
-rw-r--r--locales/ja-JP.yml3
-rw-r--r--packages/backend/migration/1707429690000-prohibited-words.js16
-rw-r--r--packages/backend/src/core/HashtagService.ts2
-rw-r--r--packages/backend/src/core/NoteCreateService.ts10
-rw-r--r--packages/backend/src/core/UtilityService.ts6
-rw-r--r--packages/backend/src/models/Meta.ts5
-rw-r--r--packages/backend/src/server/api/endpoints/admin/meta.ts8
-rw-r--r--packages/backend/src/server/api/endpoints/admin/update-meta.ts8
-rw-r--r--packages/backend/src/server/api/endpoints/notes/create.ts65
-rw-r--r--packages/backend/test/e2e/note.ts73
-rw-r--r--packages/frontend/src/pages/admin/moderation.vue8
-rw-r--r--packages/misskey-js/src/autogen/types.ts2
14 files changed, 191 insertions, 29 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index a32c557c94..1d788e1522 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -24,6 +24,8 @@
- Fix: リモートユーザーのリアクション一覧がすべて見えてしまうのを修正
* すべてのリモートユーザーのリアクション一覧を見えないようにします
- Enhance: モデレーターはすべてのユーザーのリアクション一覧を見られるように
+- Fix: 特定のキーワードを含むノートが投稿された際、エラーに出来るような設定項目を追加 #13207
+ * デフォルトは空欄なので適用前と同等の動作になります
### Client
- Feat: 新しいゲームを追加
diff --git a/locales/index.d.ts b/locales/index.d.ts
index f8c4971655..8f4c9d18e4 100644
--- a/locales/index.d.ts
+++ b/locales/index.d.ts
@@ -4181,6 +4181,18 @@ export interface Locale extends ILocale {
*/
"sensitiveWordsDescription2": string;
/**
+ * 禁止ワード
+ */
+ "prohibitedWords": string;
+ /**
+ * 設定したワードが含まれるノートを投稿しようとした際、エラーとなるようにします。改行で区切って複数設定できます。
+ */
+ "prohibitedWordsDescription": string;
+ /**
+ * スペースで区切るとAND指定になり、キーワードをスラッシュで囲むと正規表現になります。
+ */
+ "prohibitedWordsDescription2": string;
+ /**
* 非表示ハッシュタグ
*/
"hiddenTags": string;
diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml
index cf45c13f75..5348502425 100644
--- a/locales/ja-JP.yml
+++ b/locales/ja-JP.yml
@@ -1041,6 +1041,9 @@ resetPasswordConfirm: "パスワードリセットしますか?"
sensitiveWords: "センシティブワード"
sensitiveWordsDescription: "設定したワードが含まれるノートの公開範囲をホームにします。改行で区切って複数設定できます。"
sensitiveWordsDescription2: "スペースで区切るとAND指定になり、キーワードをスラッシュで囲むと正規表現になります。"
+prohibitedWords: "禁止ワード"
+prohibitedWordsDescription: "設定したワードが含まれるノートを投稿しようとした際、エラーとなるようにします。改行で区切って複数設定できます。"
+prohibitedWordsDescription2: "スペースで区切るとAND指定になり、キーワードをスラッシュで囲むと正規表現になります。"
hiddenTags: "非表示ハッシュタグ"
hiddenTagsDescription: "設定したタグをトレンドに表示させないようにします。改行で区切って複数設定できます。"
notesSearchNotAvailable: "ノート検索は利用できません。"
diff --git a/packages/backend/migration/1707429690000-prohibited-words.js b/packages/backend/migration/1707429690000-prohibited-words.js
new file mode 100644
index 0000000000..2dd62d8ff8
--- /dev/null
+++ b/packages/backend/migration/1707429690000-prohibited-words.js
@@ -0,0 +1,16 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+export class prohibitedWords1707429690000 {
+ name = 'prohibitedWords1707429690000'
+
+ async up(queryRunner) {
+ await queryRunner.query(`ALTER TABLE "meta" ADD "prohibitedWords" character varying(1024) array NOT NULL DEFAULT '{}'`);
+ }
+
+ async down(queryRunner) {
+ await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "prohibitedWords"`);
+ }
+}
diff --git a/packages/backend/src/core/HashtagService.ts b/packages/backend/src/core/HashtagService.ts
index 5a2417c9cd..712530108e 100644
--- a/packages/backend/src/core/HashtagService.ts
+++ b/packages/backend/src/core/HashtagService.ts
@@ -163,7 +163,7 @@ export class HashtagService {
const instance = await this.metaService.fetch();
const hiddenTags = instance.hiddenTags.map(t => normalizeForSearch(t));
if (hiddenTags.includes(hashtag)) return;
- if (this.utilityService.isSensitiveWordIncluded(hashtag, instance.sensitiveWords)) return;
+ if (this.utilityService.isKeyWordIncluded(hashtag, instance.sensitiveWords)) return;
// YYYYMMDDHHmm (10分間隔)
const now = new Date();
diff --git a/packages/backend/src/core/NoteCreateService.ts b/packages/backend/src/core/NoteCreateService.ts
index f7e870831d..153a6406a9 100644
--- a/packages/backend/src/core/NoteCreateService.ts
+++ b/packages/backend/src/core/NoteCreateService.ts
@@ -151,6 +151,8 @@ type Option = {
export class NoteCreateService implements OnApplicationShutdown {
#shutdownController = new AbortController();
+ public static ContainsProhibitedWordsError = class extends Error {};
+
constructor(
@Inject(DI.config)
private config: Config,
@@ -254,13 +256,19 @@ export class NoteCreateService implements OnApplicationShutdown {
if (data.visibility === 'public' && data.channel == null) {
const sensitiveWords = meta.sensitiveWords;
- if (this.utilityService.isSensitiveWordIncluded(data.cw ?? data.text ?? '', sensitiveWords)) {
+ if (this.utilityService.isKeyWordIncluded(data.cw ?? data.text ?? '', sensitiveWords)) {
data.visibility = 'home';
} else if ((await this.roleService.getUserPolicies(user.id)).canPublicNote === false) {
data.visibility = 'home';
}
}
+ if (!user.host) {
+ if (this.utilityService.isKeyWordIncluded(data.cw ?? data.text ?? '', meta.prohibitedWords)) {
+ throw new NoteCreateService.ContainsProhibitedWordsError();
+ }
+ }
+
const inSilencedInstance = this.utilityService.isSilencedHost(meta.silencedHosts, user.host);
if (data.visibility === 'public' && inSilencedInstance && user.host !== null) {
diff --git a/packages/backend/src/core/UtilityService.ts b/packages/backend/src/core/UtilityService.ts
index 5dec36c89e..15b98abe63 100644
--- a/packages/backend/src/core/UtilityService.ts
+++ b/packages/backend/src/core/UtilityService.ts
@@ -43,13 +43,13 @@ export class UtilityService {
}
@bindThis
- public isSensitiveWordIncluded(text: string, sensitiveWords: string[]): boolean {
- if (sensitiveWords.length === 0) return false;
+ public isKeyWordIncluded(text: string, keyWords: string[]): boolean {
+ if (keyWords.length === 0) return false;
if (text === '') return false;
const regexpregexp = /^\/(.+)\/(.*)$/;
- const matched = sensitiveWords.some(filter => {
+ const matched = keyWords.some(filter => {
// represents RegExp
const regexp = filter.match(regexpregexp);
// This should never happen due to input sanitisation.
diff --git a/packages/backend/src/models/Meta.ts b/packages/backend/src/models/Meta.ts
index 3265e85dd7..bcde2db0b7 100644
--- a/packages/backend/src/models/Meta.ts
+++ b/packages/backend/src/models/Meta.ts
@@ -79,6 +79,11 @@ export class MiMeta {
@Column('varchar', {
length: 1024, array: true, default: '{}',
})
+ public prohibitedWords: string[];
+
+ @Column('varchar', {
+ length: 1024, array: true, default: '{}',
+ })
public silencedHosts: string[];
@Column('varchar', {
diff --git a/packages/backend/src/server/api/endpoints/admin/meta.ts b/packages/backend/src/server/api/endpoints/admin/meta.ts
index 0627c5055c..2af9e7cd9a 100644
--- a/packages/backend/src/server/api/endpoints/admin/meta.ts
+++ b/packages/backend/src/server/api/endpoints/admin/meta.ts
@@ -156,6 +156,13 @@ export const meta = {
type: 'string',
},
},
+ prohibitedWords: {
+ type: 'array',
+ optional: false, nullable: false,
+ items: {
+ type: 'string',
+ },
+ },
bannedEmailDomains: {
type: 'array',
optional: true, nullable: false,
@@ -515,6 +522,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
blockedHosts: instance.blockedHosts,
silencedHosts: instance.silencedHosts,
sensitiveWords: instance.sensitiveWords,
+ prohibitedWords: instance.prohibitedWords,
preservedUsernames: instance.preservedUsernames,
hcaptchaSecretKey: instance.hcaptchaSecretKey,
mcaptchaSecretKey: instance.mcaptchaSecretKey,
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 d76d3dfeea..ce8c8a505d 100644
--- a/packages/backend/src/server/api/endpoints/admin/update-meta.ts
+++ b/packages/backend/src/server/api/endpoints/admin/update-meta.ts
@@ -41,6 +41,11 @@ export const paramDef = {
type: 'string',
},
},
+ prohibitedWords: {
+ 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 },
@@ -177,6 +182,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
if (Array.isArray(ps.sensitiveWords)) {
set.sensitiveWords = ps.sensitiveWords.filter(Boolean);
}
+ if (Array.isArray(ps.prohibitedWords)) {
+ set.prohibitedWords = ps.prohibitedWords.filter(Boolean);
+ }
if (Array.isArray(ps.silencedHosts)) {
let lastValue = '';
set.silencedHosts = ps.silencedHosts.sort().filter((h) => {
diff --git a/packages/backend/src/server/api/endpoints/notes/create.ts b/packages/backend/src/server/api/endpoints/notes/create.ts
index 787cda3834..50969c71cc 100644
--- a/packages/backend/src/server/api/endpoints/notes/create.ts
+++ b/packages/backend/src/server/api/endpoints/notes/create.ts
@@ -17,6 +17,8 @@ import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { NoteCreateService } from '@/core/NoteCreateService.js';
import { DI } from '@/di-symbols.js';
import { isPureRenote } from '@/misc/is-pure-renote.js';
+import { MetaService } from '@/core/MetaService.js';
+import { UtilityService } from '@/core/UtilityService.js';
import { ApiError } from '../../error.js';
export const meta = {
@@ -111,6 +113,12 @@ export const meta = {
code: 'CANNOT_RENOTE_OUTSIDE_OF_CHANNEL',
id: '33510210-8452-094c-6227-4a6c05d99f00',
},
+
+ containsProhibitedWords: {
+ message: 'Cannot post because it contains prohibited words.',
+ code: 'CONTAINS_PROHIBITED_WORDS',
+ id: 'aa6e01d3-a85c-669d-758a-76aab43af334',
+ },
},
} as const;
@@ -340,31 +348,40 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
}
// 投稿を作成
- const note = await this.noteCreateService.create(me, {
- createdAt: new Date(),
- files: files,
- poll: ps.poll ? {
- choices: ps.poll.choices,
- multiple: ps.poll.multiple ?? false,
- expiresAt: ps.poll.expiresAt ? new Date(ps.poll.expiresAt) : null,
- } : undefined,
- text: ps.text ?? undefined,
- reply,
- renote,
- cw: ps.cw,
- localOnly: ps.localOnly,
- reactionAcceptance: ps.reactionAcceptance,
- visibility: ps.visibility,
- visibleUsers,
- channel,
- apMentions: ps.noExtractMentions ? [] : undefined,
- apHashtags: ps.noExtractHashtags ? [] : undefined,
- apEmojis: ps.noExtractEmojis ? [] : undefined,
- });
+ try {
+ const note = await this.noteCreateService.create(me, {
+ createdAt: new Date(),
+ files: files,
+ poll: ps.poll ? {
+ choices: ps.poll.choices,
+ multiple: ps.poll.multiple ?? false,
+ expiresAt: ps.poll.expiresAt ? new Date(ps.poll.expiresAt) : null,
+ } : undefined,
+ text: ps.text ?? undefined,
+ reply,
+ renote,
+ cw: ps.cw,
+ localOnly: ps.localOnly,
+ reactionAcceptance: ps.reactionAcceptance,
+ visibility: ps.visibility,
+ visibleUsers,
+ channel,
+ apMentions: ps.noExtractMentions ? [] : undefined,
+ apHashtags: ps.noExtractHashtags ? [] : undefined,
+ apEmojis: ps.noExtractEmojis ? [] : undefined,
+ });
- return {
- createdNote: await this.noteEntityService.pack(note, me),
- };
+ return {
+ createdNote: await this.noteEntityService.pack(note, me),
+ };
+ } catch (e) {
+ // TODO: 他のErrorもここでキャッチしてエラーメッセージを当てるようにしたい
+ if (e instanceof NoteCreateService.ContainsProhibitedWordsError) {
+ throw new ApiError(meta.errors.containsProhibitedWords);
+ }
+
+ throw e;
+ }
});
}
}
diff --git a/packages/backend/test/e2e/note.ts b/packages/backend/test/e2e/note.ts
index 0280b051f5..1bc8cb591c 100644
--- a/packages/backend/test/e2e/note.ts
+++ b/packages/backend/test/e2e/note.ts
@@ -16,12 +16,14 @@ describe('Note', () => {
let alice: misskey.entities.SignupResponse;
let bob: misskey.entities.SignupResponse;
+ let tom: misskey.entities.SignupResponse;
beforeAll(async () => {
const connection = await initTestDb(true);
Notes = connection.getRepository(MiNote);
alice = await signup({ username: 'alice' });
bob = await signup({ username: 'bob' });
+ tom = await signup({ username: 'tom', host: 'example.com' });
}, 1000 * 60 * 2);
test('投稿できる', async () => {
@@ -607,6 +609,77 @@ describe('Note', () => {
assert.strictEqual(note2.status, 200);
assert.strictEqual(note2.body.createdNote.visibility, 'home');
});
+
+ test('禁止ワードを含む投稿はエラーになる (単語指定)', async () => {
+ const prohibited = await api('admin/update-meta', {
+ prohibitedWords: [
+ 'test',
+ ],
+ }, alice);
+
+ assert.strictEqual(prohibited.status, 204);
+
+ await new Promise(x => setTimeout(x, 2));
+
+ const note1 = await api('/notes/create', {
+ text: 'hogetesthuge',
+ }, alice);
+
+ assert.strictEqual(note1.status, 400);
+ assert.strictEqual(note1.body.error.code, 'CONTAINS_PROHIBITED_WORDS');
+ });
+
+ test('禁止ワードを含む投稿はエラーになる (正規表現)', async () => {
+ const prohibited = await api('admin/update-meta', {
+ prohibitedWords: [
+ '/Test/i',
+ ],
+ }, alice);
+
+ assert.strictEqual(prohibited.status, 204);
+
+ const note2 = await api('/notes/create', {
+ text: 'hogetesthuge',
+ }, alice);
+
+ assert.strictEqual(note2.status, 400);
+ assert.strictEqual(note2.body.error.code, 'CONTAINS_PROHIBITED_WORDS');
+ });
+
+ test('禁止ワードを含む投稿はエラーになる (スペースアンド)', async () => {
+ const prohibited = await api('admin/update-meta', {
+ prohibitedWords: [
+ 'Test hoge',
+ ],
+ }, alice);
+
+ assert.strictEqual(prohibited.status, 204);
+
+ const note2 = await api('/notes/create', {
+ text: 'hogeTesthuge',
+ }, alice);
+
+ assert.strictEqual(note2.status, 400);
+ assert.strictEqual(note2.body.error.code, 'CONTAINS_PROHIBITED_WORDS');
+ });
+
+ test('禁止ワードを含んでいてもリモートノートはエラーにならない', async () => {
+ const prohibited = await api('admin/update-meta', {
+ prohibitedWords: [
+ 'test',
+ ],
+ }, alice);
+
+ assert.strictEqual(prohibited.status, 204);
+
+ await new Promise(x => setTimeout(x, 2));
+
+ const note1 = await api('/notes/create', {
+ text: 'hogetesthuge',
+ }, tom);
+
+ assert.strictEqual(note1.status, 200);
+ });
});
describe('notes/delete', () => {
diff --git a/packages/frontend/src/pages/admin/moderation.vue b/packages/frontend/src/pages/admin/moderation.vue
index 4915bee713..248b4c53ce 100644
--- a/packages/frontend/src/pages/admin/moderation.vue
+++ b/packages/frontend/src/pages/admin/moderation.vue
@@ -40,6 +40,11 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #caption>{{ i18n.ts.sensitiveWordsDescription }}<br>{{ i18n.ts.sensitiveWordsDescription2 }}</template>
</MkTextarea>
+ <MkTextarea v-model="prohibitedWords">
+ <template #label>{{ i18n.ts.prohibitedWords }}</template>
+ <template #caption>{{ i18n.ts.prohibitedWordsDescription }}<br>{{ i18n.ts.prohibitedWordsDescription2 }}</template>
+ </MkTextarea>
+
<MkTextarea v-model="hiddenTags">
<template #label>{{ i18n.ts.hiddenTags }}</template>
<template #caption>{{ i18n.ts.hiddenTagsDescription }}</template>
@@ -76,6 +81,7 @@ import FormLink from '@/components/form/link.vue';
const enableRegistration = ref<boolean>(false);
const emailRequiredForSignup = ref<boolean>(false);
const sensitiveWords = ref<string>('');
+const prohibitedWords = ref<string>('');
const hiddenTags = ref<string>('');
const preservedUsernames = ref<string>('');
const tosUrl = ref<string | null>(null);
@@ -86,6 +92,7 @@ async function init() {
enableRegistration.value = !meta.disableRegistration;
emailRequiredForSignup.value = meta.emailRequiredForSignup;
sensitiveWords.value = meta.sensitiveWords.join('\n');
+ prohibitedWords.value = meta.prohibitedWords.join('\n');
hiddenTags.value = meta.hiddenTags.join('\n');
preservedUsernames.value = meta.preservedUsernames.join('\n');
tosUrl.value = meta.tosUrl;
@@ -99,6 +106,7 @@ function save() {
tosUrl: tosUrl.value,
privacyPolicyUrl: privacyPolicyUrl.value,
sensitiveWords: sensitiveWords.value.split('\n'),
+ prohibitedWords: prohibitedWords.value.split('\n'),
hiddenTags: hiddenTags.value.split('\n'),
preservedUsernames: preservedUsernames.value.split('\n'),
}).then(() => {
diff --git a/packages/misskey-js/src/autogen/types.ts b/packages/misskey-js/src/autogen/types.ts
index b7d65406cb..94d6673ac5 100644
--- a/packages/misskey-js/src/autogen/types.ts
+++ b/packages/misskey-js/src/autogen/types.ts
@@ -4659,6 +4659,7 @@ export type operations = {
hiddenTags: string[];
blockedHosts: string[];
sensitiveWords: string[];
+ prohibitedWords: string[];
bannedEmailDomains?: string[];
preservedUsernames: string[];
hcaptchaSecretKey: string | null;
@@ -8413,6 +8414,7 @@ export type operations = {
hiddenTags?: string[] | null;
blockedHosts?: string[] | null;
sensitiveWords?: string[] | null;
+ prohibitedWords?: string[] | null;
themeColor?: string | null;
mascotImageUrl?: string | null;
bannerUrl?: string | null;