From eb60460d28be24513b567d378cec6ecba5c158c7 Mon Sep 17 00:00:00 2001
From: tamaina <tamaina@hotmail.co.jp>
Date: Fri, 1 Mar 2024 11:57:26 +0900
Subject: [PATCH] =?UTF-8?q?enhance:=20=E7=A6=81=E6=AD=A2=E3=83=AF=E3=83=BC?=
 =?UTF-8?q?=E3=83=89=E3=83=81=E3=82=A7=E3=83=83=E3=82=AF=E5=BC=B7=E5=8C=96?=
 =?UTF-8?q?=20(#27)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

* enhance: 禁止ワードチェック強化
* リモートの禁止ワードチェックを添付ファイルとユーザーを登録する前に行うなど
  Resolve https://github.com/misskey-dev/misskey/issues/13374
* 禁止ワートの対象の見直し

* performActivityで特定のエラーが出た際にDelayedに追加しないように

* use IdentifiableError

* NoteCreateService.checkProhibitedWords

* https://github.com/misskey-dev/misskey-private/pull/27/files#r1507416135

* remove comment
---
 .../backend/src/core/NoteCreateService.ts     | 25 +++++++-
 packages/backend/src/core/UtilityService.ts   | 14 +++++
 .../src/core/activitypub/ApInboxService.ts    |  1 -
 .../core/activitypub/models/ApNoteService.ts  | 62 ++++++++++++-------
 .../queue/processors/InboxProcessorService.ts |  5 +-
 5 files changed, 83 insertions(+), 24 deletions(-)

diff --git a/packages/backend/src/core/NoteCreateService.ts b/packages/backend/src/core/NoteCreateService.ts
index 727787f868..81ae2908d3 100644
--- a/packages/backend/src/core/NoteCreateService.ts
+++ b/packages/backend/src/core/NoteCreateService.ts
@@ -263,7 +263,13 @@ export class NoteCreateService implements OnApplicationShutdown {
 			}
 		}
 
-		if (this.utilityService.isKeyWordIncluded(data.cw ?? data.text ?? '', meta.prohibitedWords)) {
+		const hasProhibitedWords = await this.checkProhibitedWordsContain({
+			cw: data.cw,
+			text: data.text,
+			pollChoices: data.poll?.choices,
+		}, meta.prohibitedWords);
+
+		if (hasProhibitedWords) {
 			throw new IdentifiableError('689ee33f-f97c-479a-ac49-1b9f8140af99', 'Note contains prohibited words');
 		}
 
@@ -995,6 +1001,23 @@ export class NoteCreateService implements OnApplicationShutdown {
 		}
 	}
 
+	public async checkProhibitedWordsContain(content: Parameters<UtilityService['concatNoteContentsForKeyWordCheck']>[0], prohibitedWords?: string[]) {
+		if (prohibitedWords == null) {
+			prohibitedWords = (await this.metaService.fetch()).prohibitedWords;
+		}
+
+		if (
+			this.utilityService.isKeyWordIncluded(
+				this.utilityService.concatNoteContentsForKeyWordCheck(content),
+				prohibitedWords,
+			)
+		) {
+			return true;
+		}
+
+		return false;
+	}
+
 	@bindThis
 	public dispose(): void {
 		this.#shutdownController.abort();
diff --git a/packages/backend/src/core/UtilityService.ts b/packages/backend/src/core/UtilityService.ts
index 638a0c019e..652e8f7449 100644
--- a/packages/backend/src/core/UtilityService.ts
+++ b/packages/backend/src/core/UtilityService.ts
@@ -42,6 +42,20 @@ export class UtilityService {
 		return silencedHosts.some(x => `.${host.toLowerCase()}`.endsWith(`.${x}`));
 	}
 
+	@bindThis
+	public concatNoteContentsForKeyWordCheck(content: {
+		cw?: string | null;
+		text?: string | null;
+		pollChoices?: string[] | null;
+		others?: string[] | null;
+	}): string {
+		/**
+		 * ノートの内容を結合してキーワードチェック用の文字列を生成する
+		 * cwとtextは内容が繋がっているかもしれないので間に何も入れずにチェックする
+		 */
+		return `${content.cw ?? ''}${content.text ?? ''}\n${(content.pollChoices ?? []).join('\n')}\n${(content.others ?? []).join('\n')}`;
+	}
+
 	@bindThis
 	public isKeyWordIncluded(text: string, keyWords: string[]): boolean {
 		if (keyWords.length === 0) return false;
diff --git a/packages/backend/src/core/activitypub/ApInboxService.ts b/packages/backend/src/core/activitypub/ApInboxService.ts
index b0f56a5d82..1621c41bcc 100644
--- a/packages/backend/src/core/activitypub/ApInboxService.ts
+++ b/packages/backend/src/core/activitypub/ApInboxService.ts
@@ -36,7 +36,6 @@ import { ApResolverService } from './ApResolverService.js';
 import { ApAudienceService } from './ApAudienceService.js';
 import { ApPersonService } from './models/ApPersonService.js';
 import { ApQuestionService } from './models/ApQuestionService.js';
-import { CacheService } from '@/core/CacheService.js';
 import { GlobalEventService } from '@/core/GlobalEventService.js';
 import type { Resolver } from './ApResolverService.js';
 import type { IAccept, IAdd, IAnnounce, IBlock, ICreate, IDelete, IFlag, IFollow, ILike, IObject, IReject, IRemove, IUndo, IUpdate, IMove } from './type.js';
diff --git a/packages/backend/src/core/activitypub/models/ApNoteService.ts b/packages/backend/src/core/activitypub/models/ApNoteService.ts
index e201b88173..b2fd435f93 100644
--- a/packages/backend/src/core/activitypub/models/ApNoteService.ts
+++ b/packages/backend/src/core/activitypub/models/ApNoteService.ts
@@ -24,6 +24,8 @@ import { StatusError } from '@/misc/status-error.js';
 import { UtilityService } from '@/core/UtilityService.js';
 import { bindThis } from '@/decorators.js';
 import { checkHttps } from '@/misc/check-https.js';
+import { IdentifiableError } from '@/misc/identifiable-error.js';
+import { isNotNull } from '@/misc/is-not-null.js';
 import { getOneApId, getApId, getOneApHrefNullable, validPost, isEmoji, getApType } from '../type.js';
 import { ApLoggerService } from '../ApLoggerService.js';
 import { ApMfmService } from '../ApMfmService.js';
@@ -37,7 +39,6 @@ import { ApQuestionService } from './ApQuestionService.js';
 import { ApImageService } from './ApImageService.js';
 import type { Resolver } from '../ApResolverService.js';
 import type { IObject, IPost } from '../type.js';
-import { isNotNull } from '@/misc/is-not-null.js';
 
 @Injectable()
 export class ApNoteService {
@@ -152,11 +153,47 @@ export class ApNoteService {
 			throw new Error('invalid note.attributedTo: ' + note.attributedTo);
 		}
 
-		const actor = await this.apPersonService.resolvePerson(getOneApId(note.attributedTo), resolver) as MiRemoteUser;
+		const uri = getOneApId(note.attributedTo);
 
-		// 投稿者が凍結されていたらスキップ
+		// ローカルで投稿者を検索し、もし凍結されていたらスキップ
+		const cachedActor = await this.apPersonService.fetchPerson(uri) as MiRemoteUser;
+		if (cachedActor && cachedActor.isSuspended) {
+			throw new IdentifiableError('85ab9bd7-3a41-4530-959d-f07073900109', 'actor has been suspended');
+		}
+
+		const apMentions = await this.apMentionService.extractApMentions(note.tag, resolver);
+		const apHashtags = extractApHashtags(note.tag);
+
+		const cw = note.summary === '' ? null : note.summary;
+
+		// テキストのパース
+		let text: string | null = null;
+		if (note.source?.mediaType === 'text/x.misskeymarkdown' && typeof note.source.content === 'string') {
+			text = note.source.content;
+		} else if (typeof note._misskey_content !== 'undefined') {
+			text = note._misskey_content;
+		} else if (typeof note.content === 'string') {
+			text = this.apMfmService.htmlToMfm(note.content, note.tag);
+		}
+
+		const poll = await this.apQuestionService.extractPollFromQuestion(note, resolver).catch(() => undefined);
+
+		//#region Contents Check
+		// 添付ファイルとユーザーをこのサーバーで登録する前に内容をチェックする
+		/**
+		 * 禁止ワードチェック
+		 */
+		const hasProhibitedWords = await this.noteCreateService.checkProhibitedWordsContain({ cw, text, pollChoices: poll?.choices });
+		if (hasProhibitedWords) {
+			throw new IdentifiableError('689ee33f-f97c-479a-ac49-1b9f8140af99', 'Note contains prohibited words');
+		}
+		//#endregion
+
+		const actor = cachedActor ?? await this.apPersonService.resolvePerson(uri, resolver) as MiRemoteUser;
+
+		// 解決した投稿者が凍結されていたらスキップ
 		if (actor.isSuspended) {
-			throw new Error('actor has been suspended');
+			throw new IdentifiableError('85ab9bd7-3a41-4530-959d-f07073900109', 'actor has been suspended');
 		}
 
 		const noteAudience = await this.apAudienceService.parseAudience(actor, note.to, note.cc, resolver);
@@ -171,9 +208,6 @@ export class ApNoteService {
 			}
 		}
 
-		const apMentions = await this.apMentionService.extractApMentions(note.tag, resolver);
-		const apHashtags = extractApHashtags(note.tag);
-
 		// 添付ファイル
 		// TODO: attachmentは必ずしもImageではない
 		// TODO: attachmentは必ずしも配列ではない
@@ -233,18 +267,6 @@ export class ApNoteService {
 			}
 		}
 
-		const cw = note.summary === '' ? null : note.summary;
-
-		// テキストのパース
-		let text: string | null = null;
-		if (note.source?.mediaType === 'text/x.misskeymarkdown' && typeof note.source.content === 'string') {
-			text = note.source.content;
-		} else if (typeof note._misskey_content !== 'undefined') {
-			text = note._misskey_content;
-		} else if (typeof note.content === 'string') {
-			text = this.apMfmService.htmlToMfm(note.content, note.tag);
-		}
-
 		// vote
 		if (reply && reply.hasPoll) {
 			const poll = await this.pollsRepository.findOneByOrFail({ noteId: reply.id });
@@ -274,8 +296,6 @@ export class ApNoteService {
 
 		const apEmojis = emojis.map(emoji => emoji.name);
 
-		const poll = await this.apQuestionService.extractPollFromQuestion(note, resolver).catch(() => undefined);
-
 		try {
 			return await this.noteCreateService.create(actor, {
 				createdAt: note.published ? new Date(note.published) : null,
diff --git a/packages/backend/src/queue/processors/InboxProcessorService.ts b/packages/backend/src/queue/processors/InboxProcessorService.ts
index 0a713149e5..3addead058 100644
--- a/packages/backend/src/queue/processors/InboxProcessorService.ts
+++ b/packages/backend/src/queue/processors/InboxProcessorService.ts
@@ -185,7 +185,10 @@ export class InboxProcessorService {
 			await this.apInboxService.performActivity(authUser.user, activity);
 		} catch (e) {
 			if (e instanceof IdentifiableError) {
-				if (e.id === '689ee33f-f97c-479a-ac49-1b9f8140af99') return 'blocked notes with prohibited words';
+				if (e.id === '689ee33f-f97c-479a-ac49-1b9f8140af99') {
+					return 'blocked notes with prohibited words';
+				}
+				if (e.id === '85ab9bd7-3a41-4530-959d-f07073900109') return 'actor has been suspended';
 			}
 			throw e;
 		}