From 075e5a1c7ae2a78033187c7a9601852f4be14637 Mon Sep 17 00:00:00 2001
From: naskya <m@naskya.net>
Date: Thu, 4 May 2023 13:17:37 +0900
Subject: [PATCH] Refactor hard word mutes

---
 packages/backend/src/misc/check-word-mute.ts  | 94 +++++++++----------
 .../src/server/api/stream/channels/antenna.ts |  7 ++
 .../src/server/api/stream/channels/channel.ts |  7 ++
 .../api/stream/channels/global-timeline.ts    |  2 +-
 .../api/stream/channels/home-timeline.ts      |  2 +-
 .../api/stream/channels/hybrid-timeline.ts    |  2 +-
 .../api/stream/channels/local-timeline.ts     |  2 +-
 .../stream/channels/recommended-timeline.ts   |  2 +-
 .../server/api/stream/channels/user-list.ts   |  7 ++
 packages/backend/src/services/note/create.ts  |  2 +-
 10 files changed, 72 insertions(+), 55 deletions(-)

diff --git a/packages/backend/src/misc/check-word-mute.ts b/packages/backend/src/misc/check-word-mute.ts
index 53193d851a..a411e46cc8 100644
--- a/packages/backend/src/misc/check-word-mute.ts
+++ b/packages/backend/src/misc/check-word-mute.ts
@@ -12,67 +12,63 @@ type UserLike = {
 	id: User["id"];
 };
 
-export type Muted = {
-	muted: boolean;
-	matched: string[];
-};
-
-const NotMuted = { muted: false, matched: [] };
-
-function escapeRegExp(x: string) {
+function escapeRegExp(x: string): string {
 	return x.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); // $& means the whole matched string
 }
 
+function checkWordMute(note: NoteLike): boolean {
+	if (note == null) return false;
+
+	const text = ((note.cw ?? "") + " " + (note.text ?? "")).trim();
+	if (text === "") return false;
+
+	for (const mutePattern of mutedWords) {
+		let mute: RE2;
+		let matched: string[];
+		if (Array.isArray(mutePattern)) {
+			matched = mutePattern.filter((keyword) => keyword !== "");
+
+			if (matched.length === 0) {
+				continue;
+			}
+			mute = new RE2(
+				`\\b${matched.map(escapeRegExp).join("\\b.*\\b")}\\b`,
+				"g",
+			);
+		} else {
+			const regexp = mutePattern.match(/^\/(.+)\/(.*)$/);
+			// This should never happen due to input sanitisation.
+			if (!regexp) {
+				console.warn(`Found invalid regex in word mutes: ${mutePattern}`);
+				continue;
+			}
+			mute = new RE2(regexp[1], regexp[2]);
+			matched = [mutePattern];
+		}
+
+		try {
+			if (mute.test(text)) return true;
+		} catch (err) {
+			// This should never happen due to input sanitisation.
+		}
+	}
+
+	return notMuted;
+}
+
 export async function getWordMute(
 	note: NoteLike,
 	me: UserLike | null | undefined,
 	mutedWords: Array<string | string[]>,
-): Promise<Muted> {
+): Promise<boolean> {
 	// 自分自身
 	if (me && note.userId === me.id) {
-		return NotMuted;
+		return false;
 	}
 
 	if (mutedWords.length > 0) {
-		const text = ((note.cw ?? "") + "\n" + (note.text ?? "")).trim();
-
-		if (text === "") {
-			return NotMuted;
-		}
-
-		for (const mutePattern of mutedWords) {
-			let mute: RE2;
-			let matched: string[];
-			if (Array.isArray(mutePattern)) {
-				matched = mutePattern.filter((keyword) => keyword !== "");
-
-				if (matched.length === 0) {
-					continue;
-				}
-				mute = new RE2(
-					`\\b${matched.map(escapeRegExp).join("\\b.*\\b")}\\b`,
-					"g",
-				);
-			} else {
-				const regexp = mutePattern.match(/^\/(.+)\/(.*)$/);
-				// This should never happen due to input sanitisation.
-				if (!regexp) {
-					console.warn(`Found invalid regex in word mutes: ${mutePattern}`);
-					continue;
-				}
-				mute = new RE2(regexp[1], regexp[2]);
-				matched = [mutePattern];
-			}
-
-			try {
-				if (mute.test(text)) {
-					return { muted: true, matched };
-				}
-			} catch (err) {
-				// This should never happen due to input sanitisation.
-			}
-		}
+		return checkWordMute(note) || checkWordMute(reply) || checkWordMute(renote)
 	}
 
-	return NotMuted;
+	return false;
 }
diff --git a/packages/backend/src/server/api/stream/channels/antenna.ts b/packages/backend/src/server/api/stream/channels/antenna.ts
index 050a8d1019..65e1eb9879 100644
--- a/packages/backend/src/server/api/stream/channels/antenna.ts
+++ b/packages/backend/src/server/api/stream/channels/antenna.ts
@@ -1,6 +1,7 @@
 import Channel from "../channel.js";
 import { Notes } from "@/models/index.js";
 import { isUserRelated } from "@/misc/is-user-related.js";
+import { getWordMute } from "@/misc/check-word-mute.js";
 import type { StreamMessages } from "../types.js";
 import { IdentifiableError } from "@/misc/identifiable-error.js";
 
@@ -37,6 +38,12 @@ export default class extends Channel {
 				if (note.renote && !note.text && isUserRelated(note, this.renoteMuting))
 					return;
 
+				if (
+					this.userProfile &&
+					(await getWordMute(note, this.user, this.userProfile.mutedWords))
+				)
+					return;
+
 				this.connection.cacheNote(note);
 
 				this.send("note", note);
diff --git a/packages/backend/src/server/api/stream/channels/channel.ts b/packages/backend/src/server/api/stream/channels/channel.ts
index d046579f42..3b242bb2a4 100644
--- a/packages/backend/src/server/api/stream/channels/channel.ts
+++ b/packages/backend/src/server/api/stream/channels/channel.ts
@@ -1,6 +1,7 @@
 import Channel from "../channel.js";
 import { Users } from "@/models/index.js";
 import { isUserRelated } from "@/misc/is-user-related.js";
+import { getWordMute } from "@/misc/check-word-mute.js";
 import type { User } from "@/models/entities/user.js";
 import type { StreamMessages } from "../types.js";
 import type { Packed } from "@/misc/schema.js";
@@ -39,6 +40,12 @@ export default class extends Channel {
 		if (note.renote && !note.text && isUserRelated(note, this.renoteMuting))
 			return;
 
+		if (
+			this.userProfile &&
+			(await getWordMute(note, this.user, this.userProfile.mutedWords))
+		)
+			return;
+
 		this.connection.cacheNote(note);
 
 		this.send("note", note);
diff --git a/packages/backend/src/server/api/stream/channels/global-timeline.ts b/packages/backend/src/server/api/stream/channels/global-timeline.ts
index aa3844c7e3..fb5c458b41 100644
--- a/packages/backend/src/server/api/stream/channels/global-timeline.ts
+++ b/packages/backend/src/server/api/stream/channels/global-timeline.ts
@@ -66,7 +66,7 @@ export default class extends Channel {
 		// そのためレコードが存在するかのチェックでは不十分なので、改めてgetWordMuteを呼んでいる
 		if (
 			this.userProfile &&
-			(await getWordMute(note, this.user, this.userProfile.mutedWords)).muted
+			(await getWordMute(note, this.user, this.userProfile.mutedWords))
 		)
 			return;
 
diff --git a/packages/backend/src/server/api/stream/channels/home-timeline.ts b/packages/backend/src/server/api/stream/channels/home-timeline.ts
index fa4a8a3901..70b2f387e1 100644
--- a/packages/backend/src/server/api/stream/channels/home-timeline.ts
+++ b/packages/backend/src/server/api/stream/channels/home-timeline.ts
@@ -64,7 +64,7 @@ export default class extends Channel {
 		// そのためレコードが存在するかのチェックでは不十分なので、改めてgetWordMuteを呼んでいる
 		if (
 			this.userProfile &&
-			(await getWordMute(note, this.user, this.userProfile.mutedWords)).muted
+			(await getWordMute(note, this.user, this.userProfile.mutedWords))
 		)
 			return;
 
diff --git a/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts b/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts
index 557bb96827..423fb461f3 100644
--- a/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts
+++ b/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts
@@ -81,7 +81,7 @@ export default class extends Channel {
 		// そのためレコードが存在するかのチェックでは不十分なので、改めてgetWordMuteを呼んでいる
 		if (
 			this.userProfile &&
-			(await getWordMute(note, this.user, this.userProfile.mutedWords)).muted
+			(await getWordMute(note, this.user, this.userProfile.mutedWords))
 		)
 			return;
 
diff --git a/packages/backend/src/server/api/stream/channels/local-timeline.ts b/packages/backend/src/server/api/stream/channels/local-timeline.ts
index dc3aab8d7d..94901661b6 100644
--- a/packages/backend/src/server/api/stream/channels/local-timeline.ts
+++ b/packages/backend/src/server/api/stream/channels/local-timeline.ts
@@ -58,7 +58,7 @@ export default class extends Channel {
 		// そのためレコードが存在するかのチェックでは不十分なので、改めてgetWordMuteを呼んでいる
 		if (
 			this.userProfile &&
-			(await getWordMute(note, this.user, this.userProfile.mutedWords)).muted
+			(await getWordMute(note, this.user, this.userProfile.mutedWords))
 		)
 			return;
 
diff --git a/packages/backend/src/server/api/stream/channels/recommended-timeline.ts b/packages/backend/src/server/api/stream/channels/recommended-timeline.ts
index 6baec77442..7eee22c325 100644
--- a/packages/backend/src/server/api/stream/channels/recommended-timeline.ts
+++ b/packages/backend/src/server/api/stream/channels/recommended-timeline.ts
@@ -79,7 +79,7 @@ export default class extends Channel {
 		// そのためレコードが存在するかのチェックでは不十分なので、改めてgetWordMuteを呼んでいる
 		if (
 			this.userProfile &&
-			(await getWordMute(note, this.user, this.userProfile.mutedWords)).muted
+			(await getWordMute(note, this.user, this.userProfile.mutedWords))
 		)
 			return;
 
diff --git a/packages/backend/src/server/api/stream/channels/user-list.ts b/packages/backend/src/server/api/stream/channels/user-list.ts
index 105c45955c..2183351e1a 100644
--- a/packages/backend/src/server/api/stream/channels/user-list.ts
+++ b/packages/backend/src/server/api/stream/channels/user-list.ts
@@ -2,6 +2,7 @@ import Channel from "../channel.js";
 import { UserListJoinings, UserLists } from "@/models/index.js";
 import type { User } from "@/models/entities/user.js";
 import { isUserRelated } from "@/misc/is-user-related.js";
+import { getWordMute } from "@/misc/check-word-mute.js";
 import type { Packed } from "@/misc/schema.js";
 
 export default class extends Channel {
@@ -59,6 +60,12 @@ export default class extends Channel {
 		if (note.renote && !note.text && isUserRelated(note, this.renoteMuting))
 			return;
 
+		if (
+			this.userProfile &&
+			(await getWordMute(note, this.user, this.userProfile.mutedWords))
+		)
+			return;
+
 		this.send("note", note);
 	}
 
diff --git a/packages/backend/src/services/note/create.ts b/packages/backend/src/services/note/create.ts
index f1164c9c6e..7c9746b73a 100644
--- a/packages/backend/src/services/note/create.ts
+++ b/packages/backend/src/services/note/create.ts
@@ -356,7 +356,7 @@ export default async (
 				for (const u of us) {
 					getWordMute(note, { id: u.userId }, u.mutedWords).then(
 						(shouldMute) => {
-							if (shouldMute.muted) {
+							if (shouldMute) {
 								MutedNotes.insert({
 									id: genId(),
 									userId: u.userId,