From 1c684cbf938ef3cfc83c46a078f054fec2b3713f Mon Sep 17 00:00:00 2001
From: tamaina <tamaina@hotmail.co.jp>
Date: Mon, 15 Apr 2019 12:10:40 +0900
Subject: [PATCH] Better permisson Fix #2341 (#4611)

* Better permisson Fix #2341

* add kinds.ts

* test

* fix

* v11

* fix
---
 locales/ja-JP.yml                             | 33 ++++++++---
 src/client/app/dev/views/apps.vue             |  2 +-
 src/client/app/dev/views/new-app.vue          | 44 ++++++--------
 src/prelude/array.ts                          |  5 ++
 src/server/api/endpoints/i/favorites.ts       |  2 +-
 src/server/api/endpoints/messaging/history.ts |  2 +-
 .../api/endpoints/messaging/messages.ts       |  2 +-
 .../endpoints/messaging/messages/create.ts    |  2 +-
 .../endpoints/messaging/messages/delete.ts    |  2 +-
 .../api/endpoints/messaging/messages/read.ts  |  2 +-
 .../api/endpoints/notes/favorites/create.ts   |  2 +-
 .../api/endpoints/notes/favorites/delete.ts   |  2 +-
 src/server/api/endpoints/notes/polls/vote.ts  |  2 +-
 src/server/api/endpoints/permissions.ts       | 29 ++++++++++
 src/server/api/kinds.ts                       | 58 +++++++++++++++++++
 src/server/api/openapi/description.ts         | 20 ++++++-
 src/server/api/openapi/gen-spec.ts            |  9 ++-
 test/api.ts                                   | 12 +++-
 test/mfm.ts                                   |  2 +-
 19 files changed, 183 insertions(+), 49 deletions(-)
 create mode 100644 src/server/api/endpoints/permissions.ts
 create mode 100644 src/server/api/kinds.ts

diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml
index cc6fe2b08..82115a722 100644
--- a/locales/ja-JP.yml
+++ b/locales/ja-JP.yml
@@ -74,10 +74,26 @@ common:
   favorites: "お気に入り"
 
   permissions:
-    'read:account': "アカウントの情報を見る"
-    'write:account': "アカウントの情報を変更する"
-    'read:drive': "ドライブを見る"
-    'write:drive': "ドライブを操作する"
+    "read:account": "アカウントの情報を見る"
+    "write:account": "アカウントの情報を変更する"
+    "read:blocks": "ブロックを見る"
+    "write:blocks": "ブロックを操作する"
+    "read:drive": "ドライブを見る"
+    "write:drive": "ドライブを操作する"
+    "read:favorites": "お気に入りを見る"
+    "write:favorites": "お気に入りを操作する"
+    "read:following": "フォローの情報を見る"
+    "write:following": "フォロー・フォロー解除する"
+    "read:messaging": "トークを見る"
+    "write:messaging": "トークを操作する"
+    "read:mutes": "ミュートを見る"
+    "write:mutes": "ミュートを操作する"
+    "write:notes": "投稿を作成・削除する"
+    "read:notifications": "通知を見る"
+    "write:notifications": "通知を操作する"
+    "read:reactions": "リアクションを見る"
+    "write:reactions": "リアクションを操作する"
+    "write:votes": "投票する"
 
   empty-timeline-info:
     follow-users-to-make-your-timeline: "ユーザーをフォローすると投稿がタイムラインに表示されます。"
@@ -1804,14 +1820,17 @@ dev/views/apps.vue:
   app-missing: "アプリなし"
 
 dev/views/new-app.vue:
+  new-app: "新しいアプリケーション"
+  new-app-info: "アプリケーションはAPIからでも作成できます。 (app/create)"
   create-app: "アプリケーションの作成"
   app-name: "アプリケーション名"
+  app-name-placeholder: "ex) Misskey for iOS"
   app-name-desc: "あなたのアプリの名称。"
-  app-name-ex: "ex) Misskey for iOS"
   app-overview: "アプリの概要"
-  app-desc: "あなたのアプリの簡単な説明や紹介。"
-  app-desc-ex: "ex) Misskey iOSクライアント。"
+  app-overview-placeholder: " ex) Misskey iOSクライアント。"
+  app-overview-desc: "あなたのアプリの簡単な説明や紹介。"
   callback-url: "コールバックURL (オプション)"
+  callback-url-placeholder: "ex) https://your.app.example.com/callback.php"
   callback-url-desc: "ユーザーが認証フォームで認証した際にリダイレクトするURLを設定できます。"
   authority: "権限"
   authority-desc: "ここで要求した機能だけがAPIからアクセスできます。"
diff --git a/src/client/app/dev/views/apps.vue b/src/client/app/dev/views/apps.vue
index 78a7cede9..b99ccdf57 100644
--- a/src/client/app/dev/views/apps.vue
+++ b/src/client/app/dev/views/apps.vue
@@ -1,6 +1,6 @@
 <template>
 <mk-ui>
-	<b-card :header="$t('header')">
+	<b-card :header="$t('manage-apps')">
 		<b-button to="/app/new" variant="primary">{{ $t('create-app') }}</b-button>
 		<hr>
 		<div class="apps">
diff --git a/src/client/app/dev/views/new-app.vue b/src/client/app/dev/views/new-app.vue
index 00f2ed60d..6b67d220a 100644
--- a/src/client/app/dev/views/new-app.vue
+++ b/src/client/app/dev/views/new-app.vue
@@ -1,35 +1,22 @@
 <template>
 <mk-ui>
-	<b-card :header="$t('header')">
+	<b-card :header="$t('new-app')">
+		<b-alert show variant="info"><fa icon="info-circle"/> {{ $t('new-app-info') }}</b-alert>
 		<b-form @submit.prevent="onSubmit" autocomplete="off">
-			<b-form-group :label="$t('app-name')" :description="$t('description')">
-				<b-form-input v-model="name" type="text" :placeholder="$t('placeholder')" autocomplete="off" required/>
+			<b-form-group :label="$t('app-name')" :description="$t('app-name-desc')">
+				<b-form-input v-model="name" type="text" :placeholder="$t('app-name-placeholder')" autocomplete="off" required/>
 			</b-form-group>
-			<b-form-group :label="$t('app-overview')" :description="$t('description')">
-				<b-textarea v-model="description" :placeholder="$t('placeholder')" autocomplete="off" required></b-textarea>
+			<b-form-group :label="$t('app-overview')" :description="$t('app-overview-desc')">
+				<b-textarea v-model="description" :placeholder="$t('app-overview-placeholder')" autocomplete="off" required></b-textarea>
 			</b-form-group>
-			<b-form-group :label="$t('callback-url')" :description="$t('description')">
-				<b-input v-model="cb" type="url" placeholder="ex) https://your.app.example.com/callback.php" autocomplete="off"/>
+			<b-form-group :label="$t('callback-url')" :description="$t('callback-url-desc')">
+				<b-input v-model="cb" type="url" :placeholder="$t('callback-url-placeholder')" autocomplete="off"/>
 			</b-form-group>
-			<b-card :header="$t('header')">
-				<b-form-group :description="$t('description')">
+			<b-card :header="$t('authority')">
+				<b-form-group :description="$t('authority-desc')">
 					<b-alert show variant="warning"><fa icon="exclamation-triangle"/> {{ $t('authority-warning') }}</b-alert>
 					<b-form-checkbox-group v-model="permission" stacked>
-						<b-form-checkbox value="read:account">{{ $t('read:account') }}</b-form-checkbox>
-						<b-form-checkbox value="write:account">{{ $t('write:account') }}</b-form-checkbox>
-						<b-form-checkbox value="write:notes">{{ $t('write:notes') }}</b-form-checkbox>
-						<b-form-checkbox value="read:reactions">{{ $t('read:reactions') }}</b-form-checkbox>
-						<b-form-checkbox value="write:reactions">{{ $t('write:reactions') }}</b-form-checkbox>
-						<b-form-checkbox value="read:following">{{ $t('read:following') }}</b-form-checkbox>
-						<b-form-checkbox value="write:following">{{ $t('write:following') }}</b-form-checkbox>
-						<b-form-checkbox value="read:mutes">{{ $t('read:mutes') }}</b-form-checkbox>
-						<b-form-checkbox value="write:mutes">{{ $t('write:mutes') }}</b-form-checkbox>
-						<b-form-checkbox value="read:blocks">{{ $t('read:blocks') }}</b-form-checkbox>
-						<b-form-checkbox value="write:blocks">{{ $t('write:blocks') }}</b-form-checkbox>
-						<b-form-checkbox value="read:drive">{{ $t('read:drive') }}</b-form-checkbox>
-						<b-form-checkbox value="write:drive">{{ $t('write:drive') }}</b-form-checkbox>
-						<b-form-checkbox value="read:notifications">{{ $t('read:notifications') }}</b-form-checkbox>
-						<b-form-checkbox value="write:notifications">{{ $t('write:notifications') }}</b-form-checkbox>
+						<b-form-checkbox v-for="v in permissionsList" :value="v" :key="v">{{ $t(`@.permissions.${v}`) }} ({{ v }})</b-form-checkbox>
 					</b-form-checkbox-group>
 				</b-form-group>
 			</b-card>
@@ -43,6 +30,7 @@
 <script lang="ts">
 import Vue from 'vue';
 import i18n from '../../i18n';
+
 export default Vue.extend({
 	i18n: i18n('dev/views/new-app.vue'),
 	data() {
@@ -51,9 +39,15 @@ export default Vue.extend({
 			description: '',
 			cb: '',
 			nidState: null,
-			permission: []
+			permission: [],
+			permissionsList: []
 		};
 	},
+	created() {
+		this.$root.api('permissions').then(permissions => {
+			this.permissionsList = permissions
+		});
+	},
 	methods: {
 		onSubmit() {
 			this.$root.api('app/create', {
diff --git a/src/prelude/array.ts b/src/prelude/array.ts
index 560dfa080..44482c57c 100644
--- a/src/prelude/array.ts
+++ b/src/prelude/array.ts
@@ -115,3 +115,8 @@ export function cumulativeSum(xs: number[]): number[] {
 	for (let i = 1; i < ys.length; i++) ys[i] += ys[i - 1];
 	return ys;
 }
+
+// Object.fromEntries()
+export function fromEntries(xs: [string, any][]): { [x: string]: any; } {
+	return xs.reduce((obj, [k, v]) => Object.assign(obj, { [k]: v }), {} as { [x: string]: any; });
+}
diff --git a/src/server/api/endpoints/i/favorites.ts b/src/server/api/endpoints/i/favorites.ts
index 2c25250be..aad706545 100644
--- a/src/server/api/endpoints/i/favorites.ts
+++ b/src/server/api/endpoints/i/favorites.ts
@@ -14,7 +14,7 @@ export const meta = {
 
 	requireCredential: true,
 
-	kind: 'favorites-read',
+	kind: 'read:favorites',
 
 	params: {
 		limit: {
diff --git a/src/server/api/endpoints/messaging/history.ts b/src/server/api/endpoints/messaging/history.ts
index c12378eb7..c2d746e48 100644
--- a/src/server/api/endpoints/messaging/history.ts
+++ b/src/server/api/endpoints/messaging/history.ts
@@ -14,7 +14,7 @@ export const meta = {
 
 	requireCredential: true,
 
-	kind: 'messaging-read',
+	kind: 'read:messaging',
 
 	params: {
 		limit: {
diff --git a/src/server/api/endpoints/messaging/messages.ts b/src/server/api/endpoints/messaging/messages.ts
index 02c57b8d0..add21e5f1 100644
--- a/src/server/api/endpoints/messaging/messages.ts
+++ b/src/server/api/endpoints/messaging/messages.ts
@@ -17,7 +17,7 @@ export const meta = {
 
 	requireCredential: true,
 
-	kind: 'messaging-read',
+	kind: 'read:messaging',
 
 	params: {
 		userId: {
diff --git a/src/server/api/endpoints/messaging/messages/create.ts b/src/server/api/endpoints/messaging/messages/create.ts
index 2c7e5ad2d..30ac0849a 100644
--- a/src/server/api/endpoints/messaging/messages/create.ts
+++ b/src/server/api/endpoints/messaging/messages/create.ts
@@ -20,7 +20,7 @@ export const meta = {
 
 	requireCredential: true,
 
-	kind: 'messaging-write',
+	kind: 'write:messaging',
 
 	params: {
 		userId: {
diff --git a/src/server/api/endpoints/messaging/messages/delete.ts b/src/server/api/endpoints/messaging/messages/delete.ts
index 9f55caba6..6a896cd8d 100644
--- a/src/server/api/endpoints/messaging/messages/delete.ts
+++ b/src/server/api/endpoints/messaging/messages/delete.ts
@@ -18,7 +18,7 @@ export const meta = {
 
 	requireCredential: true,
 
-	kind: 'messaging-write',
+	kind: 'write:messaging',
 
 	limit: {
 		duration: ms('1hour'),
diff --git a/src/server/api/endpoints/messaging/messages/read.ts b/src/server/api/endpoints/messaging/messages/read.ts
index 24a28285b..50b7f3987 100644
--- a/src/server/api/endpoints/messaging/messages/read.ts
+++ b/src/server/api/endpoints/messaging/messages/read.ts
@@ -15,7 +15,7 @@ export const meta = {
 
 	requireCredential: true,
 
-	kind: 'messaging-write',
+	kind: 'write:messaging',
 
 	params: {
 		messageId: {
diff --git a/src/server/api/endpoints/notes/favorites/create.ts b/src/server/api/endpoints/notes/favorites/create.ts
index 7e0463775..bb0c9594b 100644
--- a/src/server/api/endpoints/notes/favorites/create.ts
+++ b/src/server/api/endpoints/notes/favorites/create.ts
@@ -18,7 +18,7 @@ export const meta = {
 
 	requireCredential: true,
 
-	kind: 'favorite-write',
+	kind: 'write:favorites',
 
 	params: {
 		noteId: {
diff --git a/src/server/api/endpoints/notes/favorites/delete.ts b/src/server/api/endpoints/notes/favorites/delete.ts
index a889c84d4..49f763177 100644
--- a/src/server/api/endpoints/notes/favorites/delete.ts
+++ b/src/server/api/endpoints/notes/favorites/delete.ts
@@ -17,7 +17,7 @@ export const meta = {
 
 	requireCredential: true,
 
-	kind: 'favorite-write',
+	kind: 'write:favorites',
 
 	params: {
 		noteId: {
diff --git a/src/server/api/endpoints/notes/polls/vote.ts b/src/server/api/endpoints/notes/polls/vote.ts
index e8b8b66da..d13405597 100644
--- a/src/server/api/endpoints/notes/polls/vote.ts
+++ b/src/server/api/endpoints/notes/polls/vote.ts
@@ -26,7 +26,7 @@ export const meta = {
 
 	requireCredential: true,
 
-	kind: 'vote-write',
+	kind: 'write:votes',
 
 	params: {
 		noteId: {
diff --git a/src/server/api/endpoints/permissions.ts b/src/server/api/endpoints/permissions.ts
new file mode 100644
index 000000000..347e1e3f2
--- /dev/null
+++ b/src/server/api/endpoints/permissions.ts
@@ -0,0 +1,29 @@
+import define from '../define';
+import { kindsList } from '../kinds';
+
+export const meta = {
+	stability: 'stable',
+
+	desc: {
+		'ja-JP': 'パーミッションの一覧を返します。',
+		'en-US': 'Get the list of permissons.'
+	},
+
+	tags: ['meta'],
+
+	requireCredential: false,
+
+	params: {
+	},
+
+	res: {
+		type: 'array',
+		items: {
+			type: 'string',
+		}
+	},
+};
+
+export default define(meta, async () => {
+	return kindsList;
+});
diff --git a/src/server/api/kinds.ts b/src/server/api/kinds.ts
new file mode 100644
index 000000000..d496fa691
--- /dev/null
+++ b/src/server/api/kinds.ts
@@ -0,0 +1,58 @@
+import endpoints from './endpoints';
+import * as locale from '../../../locales/';
+import { fromEntries } from '../../prelude/array';
+
+export const kindsList = [
+	'read:account',
+	'write:account',
+	'read:blocks',
+	'write:blocks',
+	'read:drive',
+	'write:drive',
+	'read:favorites',
+	'write:favorites',
+	'read:following',
+	'write:following',
+	'read:messaging',
+	'write:messaging',
+	'read:mutes',
+	'write:mutes',
+	'write:notes',
+	'read:notifications',
+	'write:notifications',
+	'read:reactions',
+	'write:reactions',
+	'write:votes'
+];
+
+export interface IKindInfo {
+	endpoints: string[];
+	descs: { [x: string]: string; };
+}
+
+export function kinds() {
+	const kinds = fromEntries(
+		kindsList
+			.map(k => [k, {
+					endpoints: [],
+					descs: fromEntries(
+						Object.keys(locale)
+							.map(l => [l, locale[l].common.permissions[k] as string] as [string, string])
+						) as { [x: string]: string; }
+				}] as [ string, IKindInfo ])
+			) as { [x: string]: IKindInfo; };
+
+	const errors = [] as string[][];
+
+	for (const endpoint of endpoints.filter(ep => !ep.meta.secure)) {
+		if (endpoint.meta.kind) {
+			const kind = endpoint.meta.kind;
+			if (kind in kinds) kinds[kind].endpoints.push(endpoint.name);
+			else errors.push([kind, endpoint.name]);
+		}
+	}
+
+	if (errors.length > 0) throw Error('\n  ' + errors.map((e) => `Unknown kind (permission) "${e[0]}" found at ${e[1]}.`).join('\n  '));
+
+	return kinds;
+}
diff --git a/src/server/api/openapi/description.ts b/src/server/api/openapi/description.ts
index 04a0b4c71..b801c8638 100644
--- a/src/server/api/openapi/description.ts
+++ b/src/server/api/openapi/description.ts
@@ -1,6 +1,14 @@
 import config from '../../../config';
+import { IKindInfo, kinds } from '../kinds';
+
+export function getDescription(lang = 'ja-JP'): string {
+	const permissionTable = (Object.entries(kinds()) as [string, IKindInfo][])
+		.map(e => `|${e[0]}|${e[1].descs[lang]}|${e[1].endpoints.map(f => `[${f}](#operation/${f})`).join(', ')}|`)
+		.join('\n');
+
+	const descriptions = {
+		'ja-JP': `**Misskey is a decentralized microblogging platform.**
 
-export const description = `
 ## Usage
 **APIはすべてPOSTでリクエスト/レスポンスともにJSON形式です。**
 一部のAPIはリクエストに認証情報(APIキー)が必要です。リクエストの際に\`i\`というパラメータでAPIキーを添付してください。
@@ -44,4 +52,12 @@ APIキーの生成方法を擬似コードで表すと次のようになりま
 \`\`\` js
 const i = sha256(userToken + secretKey);
 \`\`\`
-`;
+
+## Permissions
+|Permisson (kind)|Description|Endpoints|
+|:--|:--|:--|
+${permissionTable}
+`
+	} as { [x: string]: string };
+	return lang in descriptions ? descriptions[lang] : descriptions['ja-JP'];
+}
diff --git a/src/server/api/openapi/gen-spec.ts b/src/server/api/openapi/gen-spec.ts
index 915fb5a6a..d194c6c8a 100644
--- a/src/server/api/openapi/gen-spec.ts
+++ b/src/server/api/openapi/gen-spec.ts
@@ -3,7 +3,7 @@ import { Context } from 'cafy';
 import config from '../../../config';
 import { errors as basicErrors } from './errors';
 import { schemas } from './schemas';
-import { description } from './description';
+import { getDescription } from './description';
 import { convertOpenApiSchema } from '../../../misc/schema';
 
 export function genOpenapiSpec(lang = 'ja-JP') {
@@ -13,7 +13,7 @@ export function genOpenapiSpec(lang = 'ja-JP') {
 		info: {
 			version: 'v1',
 			title: 'Misskey API',
-			description: '**Misskey is a decentralized microblogging platform.**\n\n' + description,
+			description: getDescription(lang),
 			'x-logo': { url: '/assets/api-doc.png' }
 		},
 
@@ -110,7 +110,10 @@ export function genOpenapiSpec(lang = 'ja-JP') {
 
 		let desc = (endpoint.meta.desc ? endpoint.meta.desc[lang] : 'No description provided.') + '\n\n';
 		desc += `**Credential required**: *${endpoint.meta.requireCredential ? 'Yes' : 'No'}*`;
-		if (endpoint.meta.kind) desc += ` / **Permission**: *${endpoint.meta.kind}*`;
+		if (endpoint.meta.kind) {
+			const kind = endpoint.meta.kind;
+			desc += ` / **Permission**: *${kind}*`;
+		}
 
 		const info = {
 			operationId: endpoint.name,
diff --git a/test/api.ts b/test/api.ts
index d14b28648..318aa8424 100644
--- a/test/api.ts
+++ b/test/api.ts
@@ -18,6 +18,8 @@ import * as assert from 'assert';
 import * as childProcess from 'child_process';
 import { async, signup, request, post, react, uploadFile } from './utils';
 
+import { kinds } from '../src/server/api/kinds';
+
 describe('API', () => {
 	let p: childProcess.ChildProcess;
 
@@ -792,7 +794,7 @@ describe('API', () => {
 				parentId: folderA.id
 			}, arisugawa);
 
-			expect(res).have.status(400);
+			assert.strictEqual(res.status, 400);
 		}));
 
 		it('存在しない親フォルダを設定できない', async(async () => {
@@ -965,5 +967,13 @@ describe('API', () => {
 			assert.strictEqual(res.body[0].id, alicePost.id);
 		}));
 	});
+
+	describe('kinds', () => {
+		it('登録されていないパーミッションを利用しているAPIがない', () => {
+			const res = kinds();
+
+			assert.strictEqual(typeof res === 'object', true);
+		});
+	});
 });
 */
diff --git a/test/mfm.ts b/test/mfm.ts
index 69260a541..8098102e9 100644
--- a/test/mfm.ts
+++ b/test/mfm.ts
@@ -1141,7 +1141,7 @@ describe('MFM', () => {
 			it('exlude emotes', () => {
 				const tokens = parse('*.*');
 				assert.deepStrictEqual(tokens, [
-					text("*.*"),
+					text('*.*'),
 				]);
 			});