From c7c08b7511f8ee86ed3d35918356d54e3ad5e8f9 Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Fri, 21 Feb 2020 00:28:45 +0900
Subject: [PATCH] Resolve #6043

---
 CHANGELOG.md                                   |  3 +++
 locales/ja-JP.yml                              |  1 +
 migration/1582210532752-antenna-exclude.ts     | 14 ++++++++++++++
 src/client/pages/my-antennas/index.antenna.vue | 12 ++++++++++--
 src/client/pages/my-antennas/index.vue         |  1 +
 src/misc/check-hit-antenna.ts                  | 13 +++++++++++++
 src/models/entities/antenna.ts                 |  5 +++++
 src/models/repositories/antenna.ts             |  1 +
 src/server/api/endpoints/antennas/create.ts    |  5 +++++
 src/server/api/endpoints/antennas/update.ts    |  5 +++++
 10 files changed, 58 insertions(+), 2 deletions(-)
 create mode 100644 migration/1582210532752-antenna-exclude.ts

diff --git a/CHANGELOG.md b/CHANGELOG.md
index cde4ad96a6..460e75b22b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,9 @@ ChangeLog
 =========
 
 -------------------
+### ✨Improvements
+* アンテナで除外キーワードを設定できるように
+
 ### 🐛Fixes
 * ハッシュタグをもっと見るできないのを修正
 * 無効になっているタイムラインでも使用できるかのように表示される問題を修正
diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml
index da255351e4..e65deb61c5 100644
--- a/locales/ja-JP.yml
+++ b/locales/ja-JP.yml
@@ -286,6 +286,7 @@ manageAntennas: "アンテナの管理"
 name: "名前"
 antennaSource: "受信ソース"
 antennaKeywords: "受信キーワード"
+antennaExcludeKeywords: "除外キーワード"
 antennaKeywordsDescription: "スペースで区切るとAND指定になり、改行で区切るとOR指定になります"
 notifyAntenna: "新しいノートを通知する"
 withFileAntenna: "ファイルが添付されたノートのみ"
diff --git a/migration/1582210532752-antenna-exclude.ts b/migration/1582210532752-antenna-exclude.ts
new file mode 100644
index 0000000000..bff47a3ec6
--- /dev/null
+++ b/migration/1582210532752-antenna-exclude.ts
@@ -0,0 +1,14 @@
+import {MigrationInterface, QueryRunner} from "typeorm";
+
+export class antennaExclude1582210532752 implements MigrationInterface {
+    name = 'antennaExclude1582210532752'
+
+    public async up(queryRunner: QueryRunner): Promise<any> {
+        await queryRunner.query(`ALTER TABLE "antenna" ADD "excludeKeywords" jsonb NOT NULL DEFAULT '[]'`, undefined);
+    }
+
+    public async down(queryRunner: QueryRunner): Promise<any> {
+        await queryRunner.query(`ALTER TABLE "antenna" DROP COLUMN "excludeKeywords"`, undefined);
+    }
+
+}
diff --git a/src/client/pages/my-antennas/index.antenna.vue b/src/client/pages/my-antennas/index.antenna.vue
index d0259a55c6..2a9aebbcbf 100644
--- a/src/client/pages/my-antennas/index.antenna.vue
+++ b/src/client/pages/my-antennas/index.antenna.vue
@@ -30,6 +30,10 @@
 			<span>{{ $t('antennaKeywords') }}</span>
 			<template #desc>{{ $t('antennaKeywordsDescription') }}</template>
 		</mk-textarea>
+		<mk-textarea v-model="excludeKeywords">
+			<span>{{ $t('antennaExcludeKeywords') }}</span>
+			<template #desc>{{ $t('antennaKeywordsDescription') }}</template>
+		</mk-textarea>
 		<mk-switch v-model="caseSensitive">{{ $t('caseSensitive') }}</mk-switch>
 		<mk-switch v-model="withFile">{{ $t('withFileAntenna') }}</mk-switch>
 		<mk-switch v-model="notify">{{ $t('notifyAntenna') }}</mk-switch>
@@ -75,6 +79,7 @@ export default Vue.extend({
 			userGroupId: null,
 			users: '',
 			keywords: '',
+			excludeKeywords: '',
 			caseSensitive: false,
 			withReplies: false,
 			withFile: false,
@@ -107,6 +112,7 @@ export default Vue.extend({
 		this.userGroupId = this.antenna.userGroupId;
 		this.users = this.antenna.users.join('\n');
 		this.keywords = this.antenna.keywords.map(x => x.join(' ')).join('\n');
+		this.excludeKeywords = this.antenna.excludeKeywords.map(x => x.join(' ')).join('\n');
 		this.caseSensitive = this.antenna.caseSensitive;
 		this.withReplies = this.antenna.withReplies;
 		this.withFile = this.antenna.withFile;
@@ -126,7 +132,8 @@ export default Vue.extend({
 					notify: this.notify,
 					caseSensitive: this.caseSensitive,
 					users: this.users.trim().split('\n').map(x => x.trim()),
-					keywords: this.keywords.trim().split('\n').map(x => x.trim().split(' '))
+					keywords: this.keywords.trim().split('\n').map(x => x.trim().split(' ')),
+					excludeKeywords: this.excludeKeywords.trim().split('\n').map(x => x.trim().split(' ')),
 				});
 				this.$emit('created');
 			} else {
@@ -141,7 +148,8 @@ export default Vue.extend({
 					notify: this.notify,
 					caseSensitive: this.caseSensitive,
 					users: this.users.trim().split('\n').map(x => x.trim()),
-					keywords: this.keywords.trim().split('\n').map(x => x.trim().split(' '))
+					keywords: this.keywords.trim().split('\n').map(x => x.trim().split(' ')),
+					excludeKeywords: this.excludeKeywords.trim().split('\n').map(x => x.trim().split(' ')),
 				});
 			}
 
diff --git a/src/client/pages/my-antennas/index.vue b/src/client/pages/my-antennas/index.vue
index 8ac70ac378..a5f6076ebf 100644
--- a/src/client/pages/my-antennas/index.vue
+++ b/src/client/pages/my-antennas/index.vue
@@ -53,6 +53,7 @@ export default Vue.extend({
 				userGroupId: null,
 				users: [],
 				keywords: [],
+				excludeKeywords: [],
 				withReplies: false,
 				caseSensitive: false,
 				withFile: false,
diff --git a/src/misc/check-hit-antenna.ts b/src/misc/check-hit-antenna.ts
index c229a07ebe..0d72c3f340 100644
--- a/src/misc/check-hit-antenna.ts
+++ b/src/misc/check-hit-antenna.ts
@@ -52,6 +52,19 @@ export async function checkHitAntenna(antenna: Antenna, note: Note, noteUser: Us
 		if (!matched) return false;
 	}
 
+	if (antenna.excludeKeywords.length > 0) {
+		if (note.text == null) return false;
+
+		const matched = antenna.excludeKeywords.some(keywords =>
+			keywords.every(keyword =>
+				antenna.caseSensitive
+					? note.text!.includes(keyword)
+					: note.text!.toLowerCase().includes(keyword.toLowerCase())
+			));
+		
+		if (matched) return false;
+	}
+
 	if (antenna.withFile) {
 		if (note.fileIds.length === 0) return false;
 	}
diff --git a/src/models/entities/antenna.ts b/src/models/entities/antenna.ts
index 7c2027b6ec..bcfe09a829 100644
--- a/src/models/entities/antenna.ts
+++ b/src/models/entities/antenna.ts
@@ -71,6 +71,11 @@ export class Antenna {
 	})
 	public keywords: string[][];
 
+	@Column('jsonb', {
+		default: []
+	})
+	public excludeKeywords: string[][];
+
 	@Column('boolean', {
 		default: false
 	})
diff --git a/src/models/repositories/antenna.ts b/src/models/repositories/antenna.ts
index 9f8aa11347..16ef2e5a39 100644
--- a/src/models/repositories/antenna.ts
+++ b/src/models/repositories/antenna.ts
@@ -21,6 +21,7 @@ export class AntennaRepository extends Repository<Antenna> {
 			createdAt: antenna.createdAt.toISOString(),
 			name: antenna.name,
 			keywords: antenna.keywords,
+			excludeKeywords: antenna.excludeKeywords,
 			src: antenna.src,
 			userListId: antenna.userListId,
 			userGroupId: userGroupJoining ? userGroupJoining.userGroupId : null,
diff --git a/src/server/api/endpoints/antennas/create.ts b/src/server/api/endpoints/antennas/create.ts
index 658b8221f2..f11b198f86 100644
--- a/src/server/api/endpoints/antennas/create.ts
+++ b/src/server/api/endpoints/antennas/create.ts
@@ -33,6 +33,10 @@ export const meta = {
 			validator: $.arr($.arr($.str))
 		},
 
+		excludeKeywords: {
+			validator: $.arr($.arr($.str))
+		},
+
 		users: {
 			validator: $.arr($.str)
 		},
@@ -102,6 +106,7 @@ export default define(meta, async (ps, user) => {
 		userListId: userList ? userList.id : null,
 		userGroupJoiningId: userGroupJoining ? userGroupJoining.id : null,
 		keywords: ps.keywords,
+		excludeKeywords: ps.excludeKeywords,
 		users: ps.users,
 		caseSensitive: ps.caseSensitive,
 		withReplies: ps.withReplies,
diff --git a/src/server/api/endpoints/antennas/update.ts b/src/server/api/endpoints/antennas/update.ts
index 520e17c4ae..ab4ce57937 100644
--- a/src/server/api/endpoints/antennas/update.ts
+++ b/src/server/api/endpoints/antennas/update.ts
@@ -36,6 +36,10 @@ export const meta = {
 			validator: $.arr($.arr($.str))
 		},
 
+		excludeKeywords: {
+			validator: $.arr($.arr($.str))
+		},
+
 		users: {
 			validator: $.arr($.str)
 		},
@@ -118,6 +122,7 @@ export default define(meta, async (ps, user) => {
 		userListId: userList ? userList.id : null,
 		userGroupJoiningId: userGroupJoining ? userGroupJoining.id : null,
 		keywords: ps.keywords,
+		excludeKeywords: ps.excludeKeywords,
 		users: ps.users,
 		caseSensitive: ps.caseSensitive,
 		withReplies: ps.withReplies,