From 00b134ce1ecfd2103677c3ed4fdda96c6748d687 Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Wed, 30 Jan 2019 17:25:56 +0900
Subject: [PATCH] Introduce silence (#4043)

* Introduce silence

* Fix icon
---
 locales/ja-JP.yml                             |  2 +
 src/client/app/admin/views/users.user.vue     |  5 +-
 src/client/app/admin/views/users.vue          | 46 ++++++++++++++++-
 src/models/user.ts                            |  5 ++
 .../api/endpoints/admin/silence-user.ts       | 49 +++++++++++++++++++
 .../api/endpoints/admin/unsilence-user.ts     | 45 +++++++++++++++++
 src/services/note/create.ts                   |  5 ++
 7 files changed, 154 insertions(+), 3 deletions(-)
 create mode 100644 src/server/api/endpoints/admin/silence-user.ts
 create mode 100644 src/server/api/endpoints/admin/unsilence-user.ts

diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml
index 91a6add5df..ed8331c523 100644
--- a/locales/ja-JP.yml
+++ b/locales/ja-JP.yml
@@ -1274,6 +1274,8 @@ admin/views/users.vue:
   unsuspend: "凍結の解除"
   unsuspend-confirm: "凍結を解除しますか?"
   unsuspended: "凍結を解除しました"
+  make-silence: "サイレンス"
+  unmake-silence: "サイレンスの解除"
   verify: "公式アカウントにする"
   verify-confirm: "公式アカウントにしますか?"
   verified: "公式アカウントにしました"
diff --git a/src/client/app/admin/views/users.user.vue b/src/client/app/admin/views/users.user.vue
index afece18e82..096e017e6a 100644
--- a/src/client/app/admin/views/users.user.vue
+++ b/src/client/app/admin/views/users.user.vue
@@ -12,6 +12,7 @@
 			<span class="is-admin" v-if="user.isAdmin">admin</span>
 			<span class="is-moderator" v-if="user.isModerator">moderator</span>
 			<span class="is-verified" v-if="user.isVerified" :title="$t('@.verified-user')"><fa icon="star"/></span>
+			<span class="is-silenced" v-if="user.isSilenced" :title="$t('@.silenced-user')"><fa :icon="faMicrophoneSlash"/></span>
 			<span class="is-suspended" v-if="user.isSuspended" :title="$t('@.suspended-user')"><fa :icon="faSnowflake"/></span>
 		</header>
 		<div>
@@ -27,6 +28,7 @@
 <script lang="ts">
 import Vue from 'vue';
 import i18n from '../../i18n';
+import { faMicrophoneSlash } from '@fortawesome/free-solid-svg-icons';
 import { faSnowflake } from '@fortawesome/free-regular-svg-icons';
 
 export default Vue.extend({
@@ -34,7 +36,7 @@ export default Vue.extend({
 	props: ['user'],
 	data() {
 		return {
-			faSnowflake
+			faSnowflake, faMicrophoneSlash
 		};
 	},
 });
@@ -76,6 +78,7 @@ export default Vue.extend({
 				color var(--noteHeaderAdminFg)
 
 			> .is-verified
+			> .is-silenced
 			> .is-suspended
 				margin 0 0 0 .5em
 				color #4dabf7
diff --git a/src/client/app/admin/views/users.vue b/src/client/app/admin/views/users.vue
index 09d074eee2..f2306c26f2 100644
--- a/src/client/app/admin/views/users.vue
+++ b/src/client/app/admin/views/users.vue
@@ -16,6 +16,10 @@
 						<ui-button @click="verifyUser" :disabled="verifying"><fa :icon="faCertificate"/> {{ $t('verify') }}</ui-button>
 						<ui-button @click="unverifyUser" :disabled="unverifying">{{ $t('unverify') }}</ui-button>
 					</ui-horizon-group>
+					<ui-horizon-group>
+						<ui-button @click="silenceUser"><fa :icon="faMicrophoneSlash"/> {{ $t('make-silence') }}</ui-button>
+						<ui-button @click="unsilenceUser">{{ $t('unmake-silence') }}</ui-button>
+					</ui-horizon-group>
 					<ui-horizon-group>
 						<ui-button @click="suspendUser" :disabled="suspending"><fa :icon="faSnowflake"/> {{ $t('suspend') }}</ui-button>
 						<ui-button @click="unsuspendUser" :disabled="unsuspending">{{ $t('unsuspend') }}</ui-button>
@@ -66,7 +70,7 @@
 import Vue from 'vue';
 import i18n from '../../i18n';
 import parseAcct from "../../../../misc/acct/parse";
-import { faCertificate, faUsers, faTerminal, faSearch, faKey, faSync } from '@fortawesome/free-solid-svg-icons';
+import { faCertificate, faUsers, faTerminal, faSearch, faKey, faSync, faMicrophoneSlash } from '@fortawesome/free-solid-svg-icons';
 import { faSnowflake } from '@fortawesome/free-regular-svg-icons';
 import XUser from './users.user.vue';
 
@@ -90,7 +94,7 @@ export default Vue.extend({
 			offset: 0,
 			users: [],
 			existMore: false,
-			faTerminal, faCertificate, faUsers, faSnowflake, faSearch, faKey, faSync
+			faTerminal, faCertificate, faUsers, faSnowflake, faSearch, faKey, faSync, faMicrophoneSlash
 		};
 	},
 
@@ -216,6 +220,44 @@ export default Vue.extend({
 			this.refreshUser();
 		},
 
+		async silenceUser() {
+			const process = async () => {
+				await this.$root.api('admin/silence-user', { userId: this.user._id });
+				this.$root.dialog({
+					type: 'success',
+					splash: true
+				});
+			};
+
+			await process().catch(e => {
+				this.$root.dialog({
+					type: 'error',
+					text: e.toString()
+				});
+			});
+
+			this.refreshUser();
+		},
+
+		async unsilenceUser() {
+			const process = async () => {
+				await this.$root.api('admin/unsilence-user', { userId: this.user._id });
+				this.$root.dialog({
+					type: 'success',
+					splash: true
+				});
+			};
+
+			await process().catch(e => {
+				this.$root.dialog({
+					type: 'error',
+					text: e.toString()
+				});
+			});
+
+			this.refreshUser();
+		},
+
 		async suspendUser() {
 			if (!await this.getConfirmed(this.$t('suspend-confirm'))) return;
 
diff --git a/src/models/user.ts b/src/models/user.ts
index 6987bd3da8..df0e3c22a2 100644
--- a/src/models/user.ts
+++ b/src/models/user.ts
@@ -54,6 +54,11 @@ type IUserBase = {
 	 */
 	isSuspended: boolean;
 
+	/**
+	 * サイレンスされているか否か
+	 */
+	isSilenced: boolean;
+
 	/**
 	 * 鍵アカウントか否か
 	 */
diff --git a/src/server/api/endpoints/admin/silence-user.ts b/src/server/api/endpoints/admin/silence-user.ts
new file mode 100644
index 0000000000..7b1090a895
--- /dev/null
+++ b/src/server/api/endpoints/admin/silence-user.ts
@@ -0,0 +1,49 @@
+import $ from 'cafy';
+import ID, { transform } from '../../../../misc/cafy-id';
+import define from '../../define';
+import User from '../../../../models/user';
+
+export const meta = {
+	desc: {
+		'ja-JP': '指定したユーザーをサイレンスにします。',
+		'en-US': 'Make silence a user.'
+	},
+
+	requireCredential: true,
+	requireModerator: true,
+
+	params: {
+		userId: {
+			validator: $.type(ID),
+			transform: transform,
+			desc: {
+				'ja-JP': '対象のユーザーID',
+				'en-US': 'The user ID which you want to make silence'
+			}
+		},
+	}
+};
+
+export default define(meta, (ps) => new Promise(async (res, rej) => {
+	const user = await User.findOne({
+		_id: ps.userId
+	});
+
+	if (user == null) {
+		return rej('user not found');
+	}
+
+	if (user.isAdmin) {
+		return rej('cannot silence admin');
+	}
+
+	await User.findOneAndUpdate({
+		_id: user._id
+	}, {
+		$set: {
+			isSilenced: true
+		}
+	});
+
+	res();
+}));
diff --git a/src/server/api/endpoints/admin/unsilence-user.ts b/src/server/api/endpoints/admin/unsilence-user.ts
new file mode 100644
index 0000000000..a01bfbb6d2
--- /dev/null
+++ b/src/server/api/endpoints/admin/unsilence-user.ts
@@ -0,0 +1,45 @@
+import $ from 'cafy';
+import ID, { transform } from '../../../../misc/cafy-id';
+import define from '../../define';
+import User from '../../../../models/user';
+
+export const meta = {
+	desc: {
+		'ja-JP': '指定したユーザーのサイレンスを解除します。',
+		'en-US': 'Unsilence a user.'
+	},
+
+	requireCredential: true,
+	requireModerator: true,
+
+	params: {
+		userId: {
+			validator: $.type(ID),
+			transform: transform,
+			desc: {
+				'ja-JP': '対象のユーザーID',
+				'en-US': 'The user ID which you want to unsilence'
+			}
+		},
+	}
+};
+
+export default define(meta, (ps) => new Promise(async (res, rej) => {
+	const user = await User.findOne({
+		_id: ps.userId
+	});
+
+	if (user == null) {
+		return rej('user not found');
+	}
+
+	await User.findOneAndUpdate({
+		_id: user._id
+	}, {
+		$set: {
+			isSilenced: false
+		}
+	});
+
+	res();
+}));
diff --git a/src/services/note/create.ts b/src/services/note/create.ts
index 344672bd63..4e8e707961 100644
--- a/src/services/note/create.ts
+++ b/src/services/note/create.ts
@@ -116,6 +116,11 @@ export default async (user: IUser, data: Option, silent = false) => new Promise<
 	if (data.viaMobile == null) data.viaMobile = false;
 	if (data.localOnly == null) data.localOnly = false;
 
+	// サイレンス
+	if (user.isSilenced && data.visibility == 'public') {
+		data.visibility = 'home';
+	}
+
 	if (data.visibleUsers) {
 		data.visibleUsers = erase(null, data.visibleUsers);
 	}