From d7c0afe65797a3f09590de1d2219a0879ede707b Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Mon, 4 Dec 2017 20:35:05 +0000
Subject: [PATCH 001/186] fix(package): update @types/chai to version 4.0.8

Closes #963
---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index b33e560c1..11533731c 100644
--- a/package.json
+++ b/package.json
@@ -26,7 +26,7 @@
 		"@prezzemolo/zip": "0.0.3",
 		"@types/bcryptjs": "2.4.1",
 		"@types/body-parser": "1.16.8",
-		"@types/chai": "4.0.6",
+		"@types/chai": "4.0.8",
 		"@types/chai-http": "3.0.3",
 		"@types/compression": "0.0.35",
 		"@types/cookie": "0.3.1",

From 9542e4b14d84493872a51f0cf47a3d5e0bd00583 Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Tue, 5 Dec 2017 17:20:23 +0000
Subject: [PATCH 002/186] fix(package): update @types/elasticsearch to version
 5.0.19

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index b33e560c1..a2a3360cf 100644
--- a/package.json
+++ b/package.json
@@ -33,7 +33,7 @@
 		"@types/cors": "2.8.3",
 		"@types/debug": "0.0.30",
 		"@types/deep-equal": "1.0.1",
-		"@types/elasticsearch": "5.0.18",
+		"@types/elasticsearch": "5.0.19",
 		"@types/event-stream": "3.3.33",
 		"@types/eventemitter3": "2.0.2",
 		"@types/express": "4.0.39",

From fe5d12587226b615b53740b8980d1252c5e4197e Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Thu, 7 Dec 2017 21:56:09 +0000
Subject: [PATCH 003/186] fix(package): update @types/redis to version 2.8.2

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index 26a953d8c..0b8fa5ce8 100644
--- a/package.json
+++ b/package.json
@@ -59,7 +59,7 @@
 		"@types/page": "1.5.32",
 		"@types/proxy-addr": "2.0.0",
 		"@types/ratelimiter": "2.1.28",
-		"@types/redis": "2.8.1",
+		"@types/redis": "2.8.2",
 		"@types/request": "2.0.8",
 		"@types/rimraf": "2.0.2",
 		"@types/riot": "3.6.1",

From d2e7546de713a9e08e45c434604461c08db8090f Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Fri, 8 Dec 2017 17:08:34 +0900
Subject: [PATCH 004/186] Update recover.html

---
 src/web/assets/recover.html | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/web/assets/recover.html b/src/web/assets/recover.html
index 35afd2adf..4922b68d3 100644
--- a/src/web/assets/recover.html
+++ b/src/web/assets/recover.html
@@ -6,7 +6,7 @@
 		<title>Misskeyのリカバリ</title>
 		<script>
 
-			const yn = window.confirm('キャッシュをクリアしますか?\n\nDo you want to clear caches?');
+			const yn = window.confirm('キャッシュをクリアしますか?(他のタブでMisskeyを開いている状態だと正常にクリアできないので、他のMisskeyのタブをすべて閉じてから行ってください)\n\nDo you want to clear caches?');
 
 			if (yn) {
 				try {

From 53db52bb406bae2492b6695c5d64a3702a4f26fa Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 8 Dec 2017 19:38:49 +0900
Subject: [PATCH 005/186] Refactor

---
 src/api/serializers/drive-file.ts | 24 +++++++++++++-----------
 1 file changed, 13 insertions(+), 11 deletions(-)

diff --git a/src/api/serializers/drive-file.ts b/src/api/serializers/drive-file.ts
index dcdaa01fa..92a9492d8 100644
--- a/src/api/serializers/drive-file.ts
+++ b/src/api/serializers/drive-file.ts
@@ -56,18 +56,20 @@ export default (
 
 	_target.url = `${config.drive_url}/${_target.id}/${encodeURIComponent(_target.name)}`;
 
-	if (opts.detail && _target.folder_id) {
-		// Populate folder
-		_target.folder = await serializeDriveFolder(_target.folder_id, {
-			detail: true
-		});
-	}
+	if (opts.detail) {
+		if (_target.folder_id) {
+			// Populate folder
+			_target.folder = await serializeDriveFolder(_target.folder_id, {
+				detail: true
+			});
+		}
 
-	if (opts.detail && _target.tags) {
-		// Populate tags
-		_target.tags = await _target.tags.map(async (tag: any) =>
-			await serializeDriveTag(tag)
-		);
+		if (_target.tags) {
+			// Populate tags
+			_target.tags = await _target.tags.map(async (tag: any) =>
+				await serializeDriveTag(tag)
+			);
+		}
 	}
 
 	resolve(_target);

From 303ccaa2f7bc6bdb52feef8c5f405f5b8fdda004 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 8 Dec 2017 19:42:02 +0900
Subject: [PATCH 006/186] Refactor

---
 src/api/common/add-file-to-drive.ts | 23 +++++++++++++----------
 1 file changed, 13 insertions(+), 10 deletions(-)

diff --git a/src/api/common/add-file-to-drive.ts b/src/api/common/add-file-to-drive.ts
index 2a649788a..dea02eeca 100644
--- a/src/api/common/add-file-to-drive.ts
+++ b/src/api/common/add-file-to-drive.ts
@@ -106,8 +106,8 @@ const addFile = async (
 		}
 	}
 
-	const [properties, folder] = await Promise.all([
-		// properties
+	const [wh, folder] = await Promise.all([
+		// Width and height (when image)
 		(async () => {
 			// 画像かどうか
 			if (!/^image\/.*$/.test(mime)) {
@@ -116,22 +116,18 @@ const addFile = async (
 
 			const imageType = mime.split('/')[1];
 
-			// 画像でもPNGかJPEGでないならスキップ
-			if (imageType != 'png' && imageType != 'jpeg') {
+			// 画像でもPNGかJPEGかGIFでないならスキップ
+			if (imageType != 'png' && imageType != 'jpeg' && imageType != 'gif') {
 				return null;
 			}
 
-			// If the file is an image, calculate width and height to save in property
+			// Calculate width and height
 			const g = gm(fs.createReadStream(path), name);
 			const size = await prominence(g).size();
-			const properties = {
-				width: size.width,
-				height: size.height
-			};
 
 			log('image width and height is calculated');
 
-			return properties;
+			return [size.width, size.height];
 		})(),
 		// folder
 		(async () => {
@@ -181,6 +177,13 @@ const addFile = async (
 
 	const readable = fs.createReadStream(path);
 
+	const properties = {};
+
+	if (wh) {
+		properties['width'] = wh[0];
+		properties['height'] = wh[1];
+	}
+
 	return addToGridFS(detectedName, readable, mime, {
 		user_id: user._id,
 		folder_id: folder !== null ? folder._id : null,

From c88097298f204bcd3f181e84e0028b2ec0a39814 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 8 Dec 2017 21:12:49 +0900
Subject: [PATCH 007/186] Display Exif

---
 gulpfile.ts                                   |  5 +-
 locales/en.yml                                |  1 +
 locales/ja.yml                                |  1 +
 package.json                                  |  2 +
 src/web/app/base.pug                          |  3 +
 src/web/app/mobile/tags/drive/file-viewer.tag | 54 ++++++++++-
 src/web/assets/code-highlight.css             | 93 +++++++++++++++++++
 7 files changed, 157 insertions(+), 2 deletions(-)
 create mode 100644 src/web/assets/code-highlight.css

diff --git a/gulpfile.ts b/gulpfile.ts
index cb7227213..641500bbe 100644
--- a/gulpfile.ts
+++ b/gulpfile.ts
@@ -3,6 +3,7 @@
  */
 
 import * as childProcess from 'child_process';
+import * as fs from 'fs';
 import * as Path from 'path';
 import * as gulp from 'gulp';
 import * as gutil from 'gulp-util';
@@ -180,7 +181,9 @@ gulp.task('build:client:pug', [
 			.pipe(pug({
 				locals: {
 					themeColor: constants.themeColor,
-					facss: fontawesome.dom.css()
+					facss: fontawesome.dom.css(),
+					//hljscss: fs.readFileSync('./node_modules/highlight.js/styles/default.css', 'utf8')
+					hljscss: fs.readFileSync('./src/web/assets/code-highlight.css', 'utf8')
 				}
 			}))
 			.pipe(htmlmin({
diff --git a/locales/en.yml b/locales/en.yml
index 9a54eed67..3009aad8c 100644
--- a/locales/en.yml
+++ b/locales/en.yml
@@ -441,6 +441,7 @@ mobile:
       rename: "Rename"
       move: "Move"
       hash: "Hash (md5)"
+      exif: "EXIF"
 
     mk-entrance-signin:
       signup: "Sign up"
diff --git a/locales/ja.yml b/locales/ja.yml
index dcf466339..cdfcd6385 100644
--- a/locales/ja.yml
+++ b/locales/ja.yml
@@ -441,6 +441,7 @@ mobile:
       rename: "名前を変更"
       move: "移動"
       hash: "ハッシュ (md5)"
+      exif: "EXIF"
 
     mk-entrance-signin:
       signup: "新規登録"
diff --git a/package.json b/package.json
index 6de160288..3bcb5f198 100644
--- a/package.json
+++ b/package.json
@@ -97,6 +97,7 @@
 		"escape-regexp": "0.0.1",
 		"event-stream": "3.3.4",
 		"eventemitter3": "3.0.0",
+		"exif-js": "^2.3.0",
 		"express": "4.16.2",
 		"file-type": "7.4.0",
 		"fuckadblock": "3.2.1",
@@ -113,6 +114,7 @@
 		"gulp-typescript": "3.2.3",
 		"gulp-uglify": "3.0.0",
 		"gulp-util": "3.0.8",
+		"highlight.js": "^9.12.0",
 		"inquirer": "4.0.1",
 		"is-root": "1.0.0",
 		"is-url": "1.2.2",
diff --git a/src/web/app/base.pug b/src/web/app/base.pug
index 140286a76..d7c7f0aed 100644
--- a/src/web/app/base.pug
+++ b/src/web/app/base.pug
@@ -24,6 +24,9 @@ html
 		//- FontAwesome style
 		style #{facss}
 
+		//- highlight.js style
+		style #{hljscss}
+
 	body
 		noscript: p
 			| JavaScriptを有効にしてください
diff --git a/src/web/app/mobile/tags/drive/file-viewer.tag b/src/web/app/mobile/tags/drive/file-viewer.tag
index da895359d..48fc83fa6 100644
--- a/src/web/app/mobile/tags/drive/file-viewer.tag
+++ b/src/web/app/mobile/tags/drive/file-viewer.tag
@@ -1,6 +1,6 @@
 <mk-drive-file-viewer>
 	<div class="preview">
-		<img if={ kind == 'image' } src={ file.url } alt={ file.name } title={ file.name }>
+		<img if={ kind == 'image' } src={ file.url } alt={ file.name } title={ file.name } onload={ onImageLoaded } ref="img">
 		<virtual if={ kind != 'image' }>%fa:file%</virtual>
 		<footer if={ kind == 'image' && file.properties && file.properties.width && file.properties.height }>
 			<span class="size">
@@ -39,6 +39,14 @@
 			</button>
 		</div>
 	</div>
+	<div class="exif" show={ exif }>
+		<div>
+			<p>
+				%fa:camera%%i18n:mobile.tags.mk-drive-file-viewer.exif%
+			</p>
+			<pre ref="exif" class="json">{ exif ? JSON.stringify(exif, null, 2) : '' }</pre>
+		</div>
+	</div>
 	<div class="hash">
 		<div>
 			<p>
@@ -178,12 +186,45 @@
 						white-space nowrap
 						overflow auto
 						font-size 0.8em
+						color #222
+						border solid 1px #dfdfdf
+						border-radius 2px
+						background #f5f5f5
+
+			> .exif
+				padding 14px
+				border-top solid 1px #dfdfdf
+
+				> div
+					max-width 500px
+					margin 0 auto
+
+					> p
+						display block
+						margin 0
+						padding 0
+						color #555
+						font-size 0.9em
+
+						> [data-fa]
+							margin-right 4px
+
+					> pre
+						display block
+						width 100%
+						margin 6px 0 0 0
+						padding 8px
+						height 128px
+						overflow auto
+						font-size 0.9em
 						border solid 1px #dfdfdf
 						border-radius 2px
 						background #f5f5f5
 
 	</style>
 	<script>
+		import EXIF from 'exif-js';
+		import hljs from 'highlight.js';
 		import bytesToSize from '../../../common/scripts/bytes-to-size';
 		import gcd from '../../../common/scripts/gcd';
 
@@ -195,6 +236,17 @@
 		this.file = this.opts.file;
 		this.kind = this.file.type.split('/')[0];
 
+		this.onImageLoaded = () => {
+			const self = this;
+			EXIF.getData(this.refs.img, function() {
+				const allMetaData = EXIF.getAllTags(this);
+				self.update({
+					exif: allMetaData
+				});
+				hljs.highlightBlock(self.refs.exif);
+			});
+		};
+
 		this.rename = () => {
 			const name = window.prompt('名前を変更', this.file.name);
 			if (name == null || name == '' || name == this.file.name) return;
diff --git a/src/web/assets/code-highlight.css b/src/web/assets/code-highlight.css
new file mode 100644
index 000000000..f0807dc9c
--- /dev/null
+++ b/src/web/assets/code-highlight.css
@@ -0,0 +1,93 @@
+.hljs {
+	font-family: Consolas, 'Courier New', Courier, Monaco, monospace;
+}
+
+.hljs,
+.hljs-subst {
+	color: #444;
+}
+
+.hljs-comment {
+	color: #888888;
+}
+
+.hljs-keyword {
+	color: #2973b7;
+}
+
+.hljs-number {
+	color: #ae81ff;
+}
+
+.hljs-string {
+	color: #e96900;
+}
+
+.hljs-regexp {
+	color: #e9003f;
+}
+
+.hljs-attribute,
+.hljs-selector-tag,
+.hljs-meta-keyword,
+.hljs-doctag,
+.hljs-name {
+	font-weight: bold;
+}
+
+.hljs-type,
+.hljs-selector-id,
+.hljs-selector-class,
+.hljs-quote,
+.hljs-template-tag,
+.hljs-deletion {
+	color: #880000;
+}
+
+.hljs-title,
+.hljs-section {
+	color: #880000;
+	font-weight: bold;
+}
+
+.hljs-symbol,
+.hljs-variable,
+.hljs-template-variable,
+.hljs-link,
+.hljs-selector-attr,
+.hljs-selector-pseudo {
+	color: #BC6060;
+}
+
+/* Language color: hue: 90; */
+
+.hljs-literal {
+	color: #78A960;
+}
+
+.hljs-built_in,
+.hljs-bullet,
+.hljs-code,
+.hljs-addition {
+	color: #397300;
+}
+
+/* Meta color: hue: 200 */
+
+.hljs-meta {
+	color: #1f7199;
+}
+
+.hljs-meta-string {
+	color: #4d99bf;
+}
+
+/* Misc effects */
+
+.hljs-emphasis {
+	font-style: italic;
+}
+
+.hljs-strong {
+	font-weight: bold;
+}

From 0de62522a885e53ef8e636b740d4f4b005114f57 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 8 Dec 2017 21:13:55 +0900
Subject: [PATCH 008/186] v3278

---
 CHANGELOG.md | 4 ++++
 package.json | 2 +-
 2 files changed, 5 insertions(+), 1 deletion(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index bc7f6b59e..3684ac57f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,10 @@ ChangeLog (Release Notes)
 =========================
 主に notable な changes を書いていきます
 
+3278 (2017/12/08)
+-----------------
+* :v:
+
 3272 (2017/12/08)
 -----------------
 * Fix bug
diff --git a/package.json b/package.json
index 3bcb5f198..6521d65e6 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
 	"name": "misskey",
 	"author": "syuilo <i@syuilo.com>",
-	"version": "0.0.3272",
+	"version": "0.0.3278",
 	"license": "MIT",
 	"description": "A miniblog-based SNS",
 	"bugs": "https://github.com/syuilo/misskey/issues",

From 6bc499f6579a9a248430748f9a69f3e5873a5ed3 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 8 Dec 2017 22:57:58 +0900
Subject: [PATCH 009/186] #967

---
 locales/en.yml                        | 14 +++++++
 locales/ja.yml                        | 14 +++++++
 package.json                          |  4 ++
 src/api/endpoints.ts                  |  8 ++++
 src/api/endpoints/i/2fa/done.ts       | 37 ++++++++++++++++++
 src/api/endpoints/i/2fa/register.ts   | 48 +++++++++++++++++++++++
 src/api/models/user.ts                |  2 +
 src/api/private/signin.ts             | 25 +++++++++++-
 src/api/serializers/user.ts           |  6 +++
 src/web/app/common/tags/signin.tag    | 11 +++++-
 src/web/app/desktop/tags/settings.tag | 56 ++++++++++++++++++++++++++-
 11 files changed, 221 insertions(+), 4 deletions(-)
 create mode 100644 src/api/endpoints/i/2fa/done.ts
 create mode 100644 src/api/endpoints/i/2fa/register.ts

diff --git a/locales/en.yml b/locales/en.yml
index 3009aad8c..6b39b4b8a 100644
--- a/locales/en.yml
+++ b/locales/en.yml
@@ -137,6 +137,7 @@ common:
     mk-signin:
       username: "Username"
       password: "Password"
+      token: "Token"
       signing-in: "Signing in..."
       signin: "Sign in"
 
@@ -295,6 +296,17 @@ desktop:
       not-match: "New password not matched"
       changed: "Password updated successfully"
 
+    mk-2fa-setting:
+      register: "Register a device"
+      enter-password: "Enter the password"
+      authenticator: "First, you need install Google Authenticator to your device:"
+      howtoinstall: "How to install"
+      scan: "Next, please scan displayed QR code:"
+      done: "Please enter the token displaying in your device:"
+      submit: "Submit"
+      success: "Setup completed successfully!"
+      failed: "Failed to setup. please ensure that the token is correct."
+
     mk-post-form:
       post-placeholder: "What's happening?"
       reply-placeholder: "Reply to this post..."
@@ -327,7 +339,9 @@ desktop:
       next: "Next post"
 
     mk-settings:
+      security: "Security"
       password: "Password"
+      2fa: "Two-factor authentication"
 
     mk-timeline-post:
       reposted-by: "Reposted by {}"
diff --git a/locales/ja.yml b/locales/ja.yml
index cdfcd6385..672d4ab40 100644
--- a/locales/ja.yml
+++ b/locales/ja.yml
@@ -137,6 +137,7 @@ common:
     mk-signin:
       username: "ユーザー名"
       password: "パスワード"
+      token: "トークン"
       signing-in: "やってます..."
       signin: "サインイン"
 
@@ -295,6 +296,17 @@ desktop:
       not-match: "新しいパスワードが一致しません"
       changed: "パスワードを変更しました"
 
+    mk-2fa-setting:
+      register: "デバイスを登録する"
+      enter-password: "パスワードを入力してください"
+      authenticator: "まず、Google Authenticatorをお使いのデバイスにインストールします:"
+      howtoinstall: "インストール方法はこちら"
+      scan: "次に、表示されているQRコードをスキャンします:"
+      done: "お使いのデバイスに表示されているトークンを入力して完了します:"
+      submit: "完了"
+      success: "設定が完了しました!"
+      failed: "設定に失敗しました。トークンに誤りがないかご確認ください。"
+
     mk-post-form:
       post-placeholder: "いまどうしてる?"
       reply-placeholder: "この投稿への返信..."
@@ -327,7 +339,9 @@ desktop:
       next: "次の投稿"
 
     mk-settings:
+      security: "セキュリティ"
       password: "パスワード"
+      2fa: "二段階認証"
 
     mk-timeline-post:
       reposted-by: "{}がRepost"
diff --git a/package.json b/package.json
index 6521d65e6..451bfc982 100644
--- a/package.json
+++ b/package.json
@@ -62,6 +62,7 @@
 		"@types/node": "8.0.57",
 		"@types/page": "1.5.32",
 		"@types/proxy-addr": "2.0.0",
+		"@types/qrcode": "^0.8.0",
 		"@types/ratelimiter": "2.1.28",
 		"@types/redis": "2.8.1",
 		"@types/request": "2.0.8",
@@ -69,6 +70,7 @@
 		"@types/riot": "3.6.1",
 		"@types/seedrandom": "2.4.27",
 		"@types/serve-favicon": "2.2.30",
+		"@types/speakeasy": "^2.0.1",
 		"@types/tmp": "0.0.33",
 		"@types/uuid": "3.4.3",
 		"@types/webpack": "3.8.1",
@@ -134,6 +136,7 @@
 		"prominence": "0.2.0",
 		"proxy-addr": "2.0.2",
 		"pug": "2.0.0-rc.4",
+		"qrcode": "^1.0.0",
 		"ratelimiter": "3.0.3",
 		"recaptcha-promise": "0.1.3",
 		"reconnecting-websocket": "3.2.2",
@@ -147,6 +150,7 @@
 		"seedrandom": "^2.4.3",
 		"serve-favicon": "2.4.5",
 		"sortablejs": "1.7.0",
+		"speakeasy": "^2.0.0",
 		"string-replace-webpack-plugin": "0.1.3",
 		"style-loader": "0.19.0",
 		"stylus": "0.54.5",
diff --git a/src/api/endpoints.ts b/src/api/endpoints.ts
index 06fb9a64a..49871c0ce 100644
--- a/src/api/endpoints.ts
+++ b/src/api/endpoints.ts
@@ -155,6 +155,14 @@ const endpoints: Endpoint[] = [
 		name: 'i',
 		withCredential: true
 	},
+	{
+		name: 'i/2fa/register',
+		withCredential: true
+	},
+	{
+		name: 'i/2fa/done',
+		withCredential: true
+	},
 	{
 		name: 'i/update',
 		withCredential: true,
diff --git a/src/api/endpoints/i/2fa/done.ts b/src/api/endpoints/i/2fa/done.ts
new file mode 100644
index 000000000..0b36033bb
--- /dev/null
+++ b/src/api/endpoints/i/2fa/done.ts
@@ -0,0 +1,37 @@
+/**
+ * Module dependencies
+ */
+import $ from 'cafy';
+import * as speakeasy from 'speakeasy';
+import User from '../../../models/user';
+
+module.exports = async (params, user) => new Promise(async (res, rej) => {
+	// Get 'token' parameter
+	const [token, tokenErr] = $(params.token).string().$;
+	if (tokenErr) return rej('invalid token param');
+
+	const _token = token.replace(/\s/g, '');
+
+	if (user.two_factor_temp_secret == null) {
+		return rej('二段階認証の設定が開始されていません');
+	}
+
+	const verified = (speakeasy as any).totp.verify({
+		secret: user.two_factor_temp_secret,
+		encoding: 'base32',
+		token: _token
+	});
+
+	if (!verified) {
+		return rej('not verified');
+	}
+
+	await User.update(user._id, {
+		$set: {
+			two_factor_secret: user.two_factor_temp_secret,
+			two_factor_enabled: true
+		}
+	});
+
+	res();
+});
diff --git a/src/api/endpoints/i/2fa/register.ts b/src/api/endpoints/i/2fa/register.ts
new file mode 100644
index 000000000..c2b5037a2
--- /dev/null
+++ b/src/api/endpoints/i/2fa/register.ts
@@ -0,0 +1,48 @@
+/**
+ * Module dependencies
+ */
+import $ from 'cafy';
+import * as bcrypt from 'bcryptjs';
+import * as speakeasy from 'speakeasy';
+import * as QRCode from 'qrcode';
+import User from '../../../models/user';
+import config from '../../../../conf';
+
+module.exports = async (params, user) => new Promise(async (res, rej) => {
+	// Get 'password' parameter
+	const [password, passwordErr] = $(params.password).string().$;
+	if (passwordErr) return rej('invalid password param');
+
+	// Compare password
+	const same = await bcrypt.compare(password, user.password);
+
+	if (!same) {
+		return rej('incorrect password');
+	}
+
+	// Generate user's secret key
+	const secret = speakeasy.generateSecret({
+		length: 32
+	});
+
+	await User.update(user._id, {
+		$set: {
+			two_factor_temp_secret: secret.base32
+		}
+	});
+
+	// Get the data URL of the authenticator URL
+	QRCode.toDataURL(speakeasy.otpauthURL({
+		secret: secret.base32,
+		encoding: 'base32',
+		label: user.username,
+		issuer: config.host
+	}), (err, data_url) => {
+		res({
+			qr: data_url,
+			secret: secret.base32,
+			label: user.username,
+			issuer: config.host
+		});
+	});
+});
diff --git a/src/api/models/user.ts b/src/api/models/user.ts
index b2f3af09f..018979158 100644
--- a/src/api/models/user.ts
+++ b/src/api/models/user.ts
@@ -72,6 +72,8 @@ export type IUser = {
 	is_pro: boolean;
 	is_suspended: boolean;
 	keywords: string[];
+	two_factor_secret: string;
+	two_factor_enabled: boolean;
 };
 
 export function init(user): IUser {
diff --git a/src/api/private/signin.ts b/src/api/private/signin.ts
index 0ebf8d6aa..7376921e2 100644
--- a/src/api/private/signin.ts
+++ b/src/api/private/signin.ts
@@ -1,5 +1,6 @@
 import * as express from 'express';
 import * as bcrypt from 'bcryptjs';
+import * as speakeasy from 'speakeasy';
 import { default as User, IUser } from '../models/user';
 import Signin from '../models/signin';
 import serialize from '../serializers/signin';
@@ -11,6 +12,7 @@ export default async (req: express.Request, res: express.Response) => {
 
 	const username = req.body['username'];
 	const password = req.body['password'];
+	const token = req.body['token'];
 
 	if (typeof username != 'string') {
 		res.sendStatus(400);
@@ -22,6 +24,11 @@ export default async (req: express.Request, res: express.Response) => {
 		return;
 	}
 
+	if (token != null && typeof token != 'string') {
+		res.sendStatus(400);
+		return;
+	}
+
 	// Fetch user
 	const user: IUser = await User.findOne({
 		username_lower: username.toLowerCase()
@@ -43,7 +50,23 @@ export default async (req: express.Request, res: express.Response) => {
 	const same = await bcrypt.compare(password, user.password);
 
 	if (same) {
-		signin(res, user, false);
+		if (user.two_factor_enabled) {
+			const verified = (speakeasy as any).totp.verify({
+				secret: user.two_factor_secret,
+				encoding: 'base32',
+				token: token
+			});
+
+			if (verified) {
+				signin(res, user, false);
+			} else {
+				res.status(400).send({
+					error: 'invalid token'
+				});
+			}
+		} else {
+			signin(res, user, false);
+		}
 	} else {
 		res.status(400).send({
 			error: 'incorrect password'
diff --git a/src/api/serializers/user.ts b/src/api/serializers/user.ts
index 3d8415660..fe924911c 100644
--- a/src/api/serializers/user.ts
+++ b/src/api/serializers/user.ts
@@ -78,6 +78,8 @@ export default (
 	// Remove private properties
 	delete _user.password;
 	delete _user.token;
+	delete _user.two_factor_temp_secret;
+	delete _user.two_factor_secret;
 	delete _user.username_lower;
 	if (_user.twitter) {
 		delete _user.twitter.access_token;
@@ -91,6 +93,10 @@ export default (
 		delete _user.client_settings;
 	}
 
+	if (!opts.detail) {
+		delete _user.two_factor_enabled;
+	}
+
 	_user.avatar_url = _user.avatar_id != null
 		? `${config.drive_url}/${_user.avatar_id}`
 		: `${config.drive_url}/default-avatar.jpg`;
diff --git a/src/web/app/common/tags/signin.tag b/src/web/app/common/tags/signin.tag
index f25d99974..f5a2be94e 100644
--- a/src/web/app/common/tags/signin.tag
+++ b/src/web/app/common/tags/signin.tag
@@ -6,6 +6,9 @@
 		<label class="password">
 			<input ref="password" type="password" placeholder="%i18n:common.tags.mk-signin.password%" required="required"/>%fa:lock%
 		</label>
+		<label class="token" if={ user && user.two_factor_enabled }>
+			<input ref="token" type="number" placeholder="%i18n:common.tags.mk-signin.token%" required="required"/>%fa:lock%
+		</label>
 		<button type="submit" disabled={ signing }>{ signing ? '%i18n:common.tags.mk-signin.signing-in%' : '%i18n:common.tags.mk-signin.signin%' }</button>
 	</form>
 	<style>
@@ -39,6 +42,7 @@
 
 					input[type=text]
 					input[type=password]
+					input[type=number]
 						user-select text
 						display inline-block
 						cursor auto
@@ -123,6 +127,10 @@
 				this.refs.password.focus();
 				return false;
 			}
+			if (this.user && this.user.two_factor_enabled && this.refs.token.value == '') {
+				this.refs.token.focus();
+				return false;
+			}
 
 			this.update({
 				signing: true
@@ -130,7 +138,8 @@
 
 			this.api('signin', {
 				username: this.refs.username.value,
-				password: this.refs.password.value
+				password: this.refs.password.value,
+				token: this.user && this.user.two_factor_enabled ? this.refs.token.value : undefined
 			}).then(() => {
 				location.reload();
 			}).catch(() => {
diff --git a/src/web/app/desktop/tags/settings.tag b/src/web/app/desktop/tags/settings.tag
index 46cd40552..5ebe28a14 100644
--- a/src/web/app/desktop/tags/settings.tag
+++ b/src/web/app/desktop/tags/settings.tag
@@ -7,7 +7,7 @@
 		<p class={ active: page == 'apps' } onmousedown={ setPage.bind(null, 'apps') }>%fa:puzzle-piece .fw%アプリ</p>
 		<p class={ active: page == 'twitter' } onmousedown={ setPage.bind(null, 'twitter') }>%fa:B twitter .fw%Twitter</p>
 		<p class={ active: page == 'signin' } onmousedown={ setPage.bind(null, 'signin') }>%fa:sign-in-alt .fw%ログイン履歴</p>
-		<p class={ active: page == 'password' } onmousedown={ setPage.bind(null, 'password') }>%fa:unlock-alt .fw%%i18n:desktop.tags.mk-settings.password%</p>
+		<p class={ active: page == 'security' } onmousedown={ setPage.bind(null, 'security') }>%fa:unlock-alt .fw%%i18n:desktop.tags.mk-settings.security%</p>
 		<p class={ active: page == 'api' } onmousedown={ setPage.bind(null, 'api') }>%fa:key .fw%API</p>
 	</div>
 	<div class="pages">
@@ -59,11 +59,16 @@
 			<mk-signin-history/>
 		</section>
 
-		<section class="password" show={ page == 'password' }>
+		<section class="password" show={ page == 'security' }>
 			<h1>%i18n:desktop.tags.mk-settings.password%</h1>
 			<mk-password-setting/>
 		</section>
 
+		<section class="2fa" show={ page == 'security' }>
+			<h1>%i18n:desktop.tags.mk-settings.2fa%</h1>
+			<mk-2fa-setting/>
+		</section>
+
 		<section class="api" show={ page == 'api' }>
 			<h1>API</h1>
 			<mk-api-info/>
@@ -285,3 +290,50 @@
 		};
 	</script>
 </mk-password-setting>
+
+<mk-2fa-setting>
+	<p><button onclick={ register }>%i18n:desktop.tags.mk-2fa-setting.register%</button></p>
+	<div if={ data }>
+		<ol>
+			<li>%i18n:desktop.tags.mk-2fa-setting.authenticator% <a href="https://support.google.com/accounts/answer/1066447" target="_blank">%i18n:desktop.tags.mk-2fa-setting.howtoinstall%</a></li>
+			<li>%i18n:desktop.tags.mk-2fa-setting.scan%<br><img src={ data.qr }></li>
+			<li>%i18n:desktop.tags.mk-2fa-setting.done%<br>
+				<input type="number" ref="token"><button onclick={ submit }>%i18n:desktop.tags.mk-2fa-setting.submit%</button>
+			</li>
+		</ol>
+	</div>
+	<style>
+		:scope
+			display block
+			color #4a535a
+
+	</style>
+	<script>
+		import passwordDialog from '../scripts/password-dialog';
+		import notify from '../scripts/notify';
+
+		this.mixin('api');
+
+		this.register = () => {
+			passwordDialog('%i18n:desktop.tags.mk-2fa-setting.enter-password%', password => {
+				this.api('i/2fa/register', {
+					password: password
+				}).then(data => {
+					this.update({
+						data: data
+					});
+				});
+			});
+		};
+
+		this.submit = () => {
+			this.api('i/2fa/done', {
+				token: this.refs.token.value
+			}).then(() => {
+				notify('%i18n:desktop.tags.mk-2fa-setting.success%');
+			}).catch(() => {
+				notify('%i18n:desktop.tags.mk-2fa-setting.failed%');
+			});
+		};
+	</script>
+</mk-2fa-setting>

From 9ed68439e550d0f3d95b258df114598e1a05cd92 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 8 Dec 2017 23:13:03 +0900
Subject: [PATCH 010/186] :v:

---
 locales/en.yml                        | 3 +++
 locales/ja.yml                        | 3 +++
 src/web/app/desktop/tags/settings.tag | 3 +++
 3 files changed, 9 insertions(+)

diff --git a/locales/en.yml b/locales/en.yml
index 6b39b4b8a..020fc3949 100644
--- a/locales/en.yml
+++ b/locales/en.yml
@@ -297,6 +297,8 @@ desktop:
       changed: "Password updated successfully"
 
     mk-2fa-setting:
+      intro: "If you set up 2-step verification, you will need not only a password at sign-in but also a pre-registered physical device (such as your smartphone), which will improve security."
+      caution: "As a caveat, security improves, but you can not sign in to Misskey if you lose a registered device, etc."
       register: "Register a device"
       enter-password: "Enter the password"
       authenticator: "First, you need install Google Authenticator to your device:"
@@ -306,6 +308,7 @@ desktop:
       submit: "Submit"
       success: "Setup completed successfully!"
       failed: "Failed to setup. please ensure that the token is correct."
+      info: "From the next sign in, enter the token that is displayed on the device in addition to the password."
 
     mk-post-form:
       post-placeholder: "What's happening?"
diff --git a/locales/ja.yml b/locales/ja.yml
index 672d4ab40..27804aa7c 100644
--- a/locales/ja.yml
+++ b/locales/ja.yml
@@ -297,6 +297,8 @@ desktop:
       changed: "パスワードを変更しました"
 
     mk-2fa-setting:
+      intro: "二段階認証を設定すると、サインイン時にパスワードだけでなく、予め登録しておいた物理的なデバイス(例えばあなたのスマートフォンなど)も必要になり、よりセキュリティが向上します。"
+      caution: "注意点として、セキュリティは向上しますが、登録したデバイスを紛失するなどした場合、Misskeyにサインインできなくなります。"
       register: "デバイスを登録する"
       enter-password: "パスワードを入力してください"
       authenticator: "まず、Google Authenticatorをお使いのデバイスにインストールします:"
@@ -306,6 +308,7 @@ desktop:
       submit: "完了"
       success: "設定が完了しました!"
       failed: "設定に失敗しました。トークンに誤りがないかご確認ください。"
+      info: "次回サインインからは、同様にパスワードに加えてデバイスに表示されているトークンを入力します。"
 
     mk-post-form:
       post-placeholder: "いまどうしてる?"
diff --git a/src/web/app/desktop/tags/settings.tag b/src/web/app/desktop/tags/settings.tag
index 5ebe28a14..2c38f1352 100644
--- a/src/web/app/desktop/tags/settings.tag
+++ b/src/web/app/desktop/tags/settings.tag
@@ -292,6 +292,8 @@
 </mk-password-setting>
 
 <mk-2fa-setting>
+	<p>%i18n:desktop.tags.mk-2fa-setting.intro%</p>
+	<p>%i18n:desktop.tags.mk-2fa-setting.caution%</p>
 	<p><button onclick={ register }>%i18n:desktop.tags.mk-2fa-setting.register%</button></p>
 	<div if={ data }>
 		<ol>
@@ -300,6 +302,7 @@
 			<li>%i18n:desktop.tags.mk-2fa-setting.done%<br>
 				<input type="number" ref="token"><button onclick={ submit }>%i18n:desktop.tags.mk-2fa-setting.submit%</button>
 			</li>
+			<li>%i18n:desktop.tags.mk-2fa-setting.info%</li>
 		</ol>
 	</div>
 	<style>

From b6380f52a9bfa9ae85f636c46eafb2af53559ce1 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 8 Dec 2017 23:13:44 +0900
Subject: [PATCH 011/186] v3281

---
 CHANGELOG.md | 4 ++++
 package.json | 2 +-
 2 files changed, 5 insertions(+), 1 deletion(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3684ac57f..48372e34c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,10 @@ ChangeLog (Release Notes)
 =========================
 主に notable な changes を書いていきます
 
+3281 (2017/12/08)
+-----------------
+* 二段階認証の実装 (#967)
+
 3278 (2017/12/08)
 -----------------
 * :v:
diff --git a/package.json b/package.json
index 451bfc982..e5f8c824d 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
 	"name": "misskey",
 	"author": "syuilo <i@syuilo.com>",
-	"version": "0.0.3278",
+	"version": "0.0.3281",
 	"license": "MIT",
 	"description": "A miniblog-based SNS",
 	"bugs": "https://github.com/syuilo/misskey/issues",

From 9d2ec375699fc54f93852bbb54116bdd8e53298e Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Fri, 8 Dec 2017 23:34:33 +0900
Subject: [PATCH 012/186] Update ja.yml

---
 locales/ja.yml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/locales/ja.yml b/locales/ja.yml
index 27804aa7c..c5996c76b 100644
--- a/locales/ja.yml
+++ b/locales/ja.yml
@@ -298,7 +298,7 @@ desktop:
 
     mk-2fa-setting:
       intro: "二段階認証を設定すると、サインイン時にパスワードだけでなく、予め登録しておいた物理的なデバイス(例えばあなたのスマートフォンなど)も必要になり、よりセキュリティが向上します。"
-      caution: "注意点として、セキュリティは向上しますが、登録したデバイスを紛失するなどした場合、Misskeyにサインインできなくなります。"
+      caution: "登録したデバイスを紛失するなどした場合、Misskeyにサインインできなくなりますのでご注意ください。"
       register: "デバイスを登録する"
       enter-password: "パスワードを入力してください"
       authenticator: "まず、Google Authenticatorをお使いのデバイスにインストールします:"

From 1a80fdeaae43c63f9650fb247c36d88123ef5287 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 9 Dec 2017 01:22:20 +0900
Subject: [PATCH 013/186] Remove ^s

---
 package.json | 22 +++++++++++-----------
 1 file changed, 11 insertions(+), 11 deletions(-)

diff --git a/package.json b/package.json
index e5f8c824d..0a40e32e3 100644
--- a/package.json
+++ b/package.json
@@ -22,10 +22,10 @@
 		"format": "gulp format"
 	},
 	"dependencies": {
-		"@fortawesome/fontawesome": "^1.0.0",
-		"@fortawesome/fontawesome-free-brands": "^5.0.0",
-		"@fortawesome/fontawesome-free-regular": "^5.0.0",
-		"@fortawesome/fontawesome-free-solid": "^5.0.0",
+		"@fortawesome/fontawesome": "1.0.0",
+		"@fortawesome/fontawesome-free-brands": "5.0.0",
+		"@fortawesome/fontawesome-free-regular": "5.0.0",
+		"@fortawesome/fontawesome-free-solid": "5.0.0",
 		"@prezzemolo/rap": "0.1.2",
 		"@prezzemolo/zip": "0.0.3",
 		"@types/bcryptjs": "2.4.1",
@@ -62,7 +62,7 @@
 		"@types/node": "8.0.57",
 		"@types/page": "1.5.32",
 		"@types/proxy-addr": "2.0.0",
-		"@types/qrcode": "^0.8.0",
+		"@types/qrcode": "0.8.0",
 		"@types/ratelimiter": "2.1.28",
 		"@types/redis": "2.8.1",
 		"@types/request": "2.0.8",
@@ -70,7 +70,7 @@
 		"@types/riot": "3.6.1",
 		"@types/seedrandom": "2.4.27",
 		"@types/serve-favicon": "2.2.30",
-		"@types/speakeasy": "^2.0.1",
+		"@types/speakeasy": "2.0.1",
 		"@types/tmp": "0.0.33",
 		"@types/uuid": "3.4.3",
 		"@types/webpack": "3.8.1",
@@ -99,7 +99,7 @@
 		"escape-regexp": "0.0.1",
 		"event-stream": "3.3.4",
 		"eventemitter3": "3.0.0",
-		"exif-js": "^2.3.0",
+		"exif-js": "2.3.0",
 		"express": "4.16.2",
 		"file-type": "7.4.0",
 		"fuckadblock": "3.2.1",
@@ -116,7 +116,7 @@
 		"gulp-typescript": "3.2.3",
 		"gulp-uglify": "3.0.0",
 		"gulp-util": "3.0.8",
-		"highlight.js": "^9.12.0",
+		"highlight.js": "9.12.0",
 		"inquirer": "4.0.1",
 		"is-root": "1.0.0",
 		"is-url": "1.2.2",
@@ -136,7 +136,7 @@
 		"prominence": "0.2.0",
 		"proxy-addr": "2.0.2",
 		"pug": "2.0.0-rc.4",
-		"qrcode": "^1.0.0",
+		"qrcode": "1.0.0",
 		"ratelimiter": "3.0.3",
 		"recaptcha-promise": "0.1.3",
 		"reconnecting-websocket": "3.2.2",
@@ -147,10 +147,10 @@
 		"riot-tag-loader": "1.0.0",
 		"rndstr": "1.0.0",
 		"s-age": "1.1.0",
-		"seedrandom": "^2.4.3",
+		"seedrandom": "2.4.3",
 		"serve-favicon": "2.4.5",
 		"sortablejs": "1.7.0",
-		"speakeasy": "^2.0.0",
+		"speakeasy": "2.0.0",
 		"string-replace-webpack-plugin": "0.1.3",
 		"style-loader": "0.19.0",
 		"stylus": "0.54.5",

From 03a35b3fd52343959e27d4a80245fd4052b6517d Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 9 Dec 2017 01:51:05 +0900
Subject: [PATCH 014/186] #297

---
 src/web/app/mobile/tags/drive.tag        |  10 +-
 src/web/app/mobile/tags/drive/file.tag   | 195 ++++++++++++-----------
 src/web/app/mobile/tags/drive/folder.tag |  57 ++++---
 3 files changed, 139 insertions(+), 123 deletions(-)

diff --git a/src/web/app/mobile/tags/drive.tag b/src/web/app/mobile/tags/drive.tag
index 8350ce07e..a72c8d51c 100644
--- a/src/web/app/mobile/tags/drive.tag
+++ b/src/web/app/mobile/tags/drive.tag
@@ -1,9 +1,9 @@
 <mk-drive>
 	<nav ref="nav">
-		<p onclick={ goRoot }>%fa:cloud%%i18n:mobile.tags.mk-drive.drive%</p>
+		<a onclick={ goRoot } href="/i/drive">%fa:cloud%%i18n:mobile.tags.mk-drive.drive%</a>
 		<virtual each={ folder in hierarchyFolders }>
 			<span>%fa:angle-right%</span>
-			<p onclick={ move }>{ folder.name }</p>
+			<a onclick={ move } href="/i/drive/folder/{ folder.id }">{ folder.name }</a>
 		</virtual>
 		<virtual if={ folder != null }>
 			<span>%fa:angle-right%</span>
@@ -74,9 +74,12 @@
 				border-bottom solid 1px rgba(0, 0, 0, 0.13)
 
 				> p
+				> a
 					display inline
 					margin 0
 					padding 0
+					text-decoration none !important
+					color inherit
 
 					&:last-child
 						font-weight bold
@@ -246,6 +249,7 @@
 
 		this.move = ev => {
 			this.cd(ev.item.folder);
+			return false;
 		};
 
 		this.cd = (target, silent = false) => {
@@ -339,6 +343,8 @@
 				this.trigger('move-root');
 				this.fetch();
 			}
+
+			return false;
 		};
 
 		this.fetch = () => {
diff --git a/src/web/app/mobile/tags/drive/file.tag b/src/web/app/mobile/tags/drive/file.tag
index 93a8dba7e..0b3506a43 100644
--- a/src/web/app/mobile/tags/drive/file.tag
+++ b/src/web/app/mobile/tags/drive/file.tag
@@ -1,119 +1,123 @@
-<mk-drive-file onclick={ onclick } data-is-selected={ isSelected }>
-	<div class="container">
-		<div class="thumbnail" style={ 'background-image: url(' + file.url + '?thumbnail&size=128)' }></div>
-		<div class="body">
-			<p class="name"><span>{ file.name.lastIndexOf('.') != -1 ? file.name.substr(0, file.name.lastIndexOf('.')) : file.name }</span><span class="ext" if={ file.name.lastIndexOf('.') != -1 }>{ file.name.substr(file.name.lastIndexOf('.')) }</span></p>
-			<!--
-			if file.tags.length > 0
-				ul.tags
-					each tag in file.tags
-						li.tag(style={background: tag.color, color: contrast(tag.color)})= tag.name
-			-->
-			<footer>
-				<p class="type"><mk-file-type-icon type={ file.type }/>{ file.type }</p>
-				<p class="separator"></p>
-				<p class="data-size">{ bytesToSize(file.datasize) }</p>
-				<p class="separator"></p>
-				<p class="created-at">
-					%fa:R clock%<mk-time time={ file.created_at }/>
-				</p>
-			</footer>
+<mk-drive-file data-is-selected={ isSelected }>
+	<a onclick={ onclick } href="/i/drive/file/{ file.id }">
+		<div class="container">
+			<div class="thumbnail" style={ 'background-image: url(' + file.url + '?thumbnail&size=128)' }></div>
+			<div class="body">
+				<p class="name"><span>{ file.name.lastIndexOf('.') != -1 ? file.name.substr(0, file.name.lastIndexOf('.')) : file.name }</span><span class="ext" if={ file.name.lastIndexOf('.') != -1 }>{ file.name.substr(file.name.lastIndexOf('.')) }</span></p>
+				<!--
+				if file.tags.length > 0
+					ul.tags
+						each tag in file.tags
+							li.tag(style={background: tag.color, color: contrast(tag.color)})= tag.name
+				-->
+				<footer>
+					<p class="type"><mk-file-type-icon type={ file.type }/>{ file.type }</p>
+					<p class="separator"></p>
+					<p class="data-size">{ bytesToSize(file.datasize) }</p>
+					<p class="separator"></p>
+					<p class="created-at">
+						%fa:R clock%<mk-time time={ file.created_at }/>
+					</p>
+				</footer>
+			</div>
 		</div>
-	</div>
+	</a>
 	<style>
 		:scope
 			display block
 
-			&, *
-				user-select none
+			> a
+				display block
+				text-decoration none !important
 
-			*
-				pointer-events none
+				*
+					user-select none
+					pointer-events none
 
-			> .container
-				max-width 500px
-				margin 0 auto
-				padding 16px
+				> .container
+					max-width 500px
+					margin 0 auto
+					padding 16px
 
-				&:after
-					content ""
-					display block
-					clear both
-
-				> .thumbnail
-					display block
-					float left
-					width 64px
-					height 64px
-					background-size cover
-					background-position center center
-
-				> .body
-					display block
-					float left
-					width calc(100% - 74px)
-					margin-left 10px
-
-					> .name
+					&:after
+						content ""
 						display block
-						margin 0
-						padding 0
-						font-size 0.9em
-						font-weight bold
-						color #555
-						text-overflow ellipsis
-						overflow-wrap break-word
+						clear both
 
-						> .ext
-							opacity 0.5
-
-					> .tags
+					> .thumbnail
 						display block
-						margin 4px 0 0 0
-						padding 0
-						list-style none
-						font-size 0.5em
+						float left
+						width 64px
+						height 64px
+						background-size cover
+						background-position center center
 
-						> .tag
-							display inline-block
-							margin 0 5px 0 0
-							padding 1px 5px
-							border-radius 2px
-
-					> footer
+					> .body
 						display block
-						margin 4px 0 0 0
-						font-size 0.7em
+						float left
+						width calc(100% - 74px)
+						margin-left 10px
 
-						> .separator
-							display inline
-							margin 0
-							padding 0 4px
-							color #CDCDCD
-
-						> .type
-							display inline
+						> .name
+							display block
 							margin 0
 							padding 0
-							color #9D9D9D
+							font-size 0.9em
+							font-weight bold
+							color #555
+							text-overflow ellipsis
+							overflow-wrap break-word
 
-							> mk-file-type-icon
-								margin-right 4px
+							> .ext
+								opacity 0.5
 
-						> .data-size
-							display inline
-							margin 0
+						> .tags
+							display block
+							margin 4px 0 0 0
 							padding 0
-							color #9D9D9D
+							list-style none
+							font-size 0.5em
 
-						> .created-at
-							display inline
-							margin 0
-							padding 0
-							color #BDBDBD
+							> .tag
+								display inline-block
+								margin 0 5px 0 0
+								padding 1px 5px
+								border-radius 2px
 
-							> [data-fa]
-								margin-right 2px
+						> footer
+							display block
+							margin 4px 0 0 0
+							font-size 0.7em
+
+							> .separator
+								display inline
+								margin 0
+								padding 0 4px
+								color #CDCDCD
+
+							> .type
+								display inline
+								margin 0
+								padding 0
+								color #9D9D9D
+
+								> mk-file-type-icon
+									margin-right 4px
+
+							> .data-size
+								display inline
+								margin 0
+								padding 0
+								color #9D9D9D
+
+							> .created-at
+								display inline
+								margin 0
+								padding 0
+								color #BDBDBD
+
+								> [data-fa]
+									margin-right 2px
 
 			&[data-is-selected]
 				background $theme-color
@@ -136,6 +140,7 @@
 
 		this.onclick = () => {
 			this.browser.chooseFile(this.file);
+			return false;
 		};
 	</script>
 </mk-drive-file>
diff --git a/src/web/app/mobile/tags/drive/folder.tag b/src/web/app/mobile/tags/drive/folder.tag
index 196e7e326..785847a9c 100644
--- a/src/web/app/mobile/tags/drive/folder.tag
+++ b/src/web/app/mobile/tags/drive/folder.tag
@@ -1,39 +1,43 @@
 <mk-drive-folder onclick={ onclick }>
-	<div class="container">
-		<p class="name">%fa:folder%{ folder.name }</p>%fa:angle-right%
-	</div>
+	<a onclick={ onclick } href="/i/drive/folder/{ folder.id }">
+		<div class="container">
+			<p class="name">%fa:folder%{ folder.name }</p>%fa:angle-right%
+		</div>
+	</a>
 	<style>
 		:scope
 			display block
-			color #777
 
-			&, *
-				user-select none
+			> a
+				display block
+				color #777
+				text-decoration none !important
 
-			*
-				pointer-events none
+				*
+					user-select none
+					pointer-events none
 
-			> .container
-				max-width 500px
-				margin 0 auto
-				padding 16px
+				> .container
+					max-width 500px
+					margin 0 auto
+					padding 16px
 
-				> .name
-					display block
-					margin 0
-					padding 0
+					> .name
+						display block
+						margin 0
+						padding 0
+
+						> [data-fa]
+							margin-right 6px
 
 					> [data-fa]
-						margin-right 6px
-
-				> [data-fa]
-					position absolute
-					top 0
-					bottom 0
-					right 8px
-					margin auto 0 auto 0
-					width 1em
-					height 1em
+						position absolute
+						top 0
+						bottom 0
+						right 8px
+						margin auto 0 auto 0
+						width 1em
+						height 1em
 
 	</style>
 	<script>
@@ -42,6 +46,7 @@
 
 		this.onclick = () => {
 			this.browser.cd(this.folder);
+			return false;
 		};
 	</script>
 </mk-drive-folder>

From b12c4a061135c3c6ef9a96184217deaeb63377c8 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 9 Dec 2017 01:53:17 +0900
Subject: [PATCH 015/186] v3292

---
 CHANGELOG.md | 4 ++++
 package.json | 2 +-
 2 files changed, 5 insertions(+), 1 deletion(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 48372e34c..718c2d4ba 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,10 @@ ChangeLog (Release Notes)
 =========================
 主に notable な changes を書いていきます
 
+3292 (2017/12/09)
+-----------------
+* ユーザビリティの向上
+
 3281 (2017/12/08)
 -----------------
 * 二段階認証の実装 (#967)
diff --git a/package.json b/package.json
index 0f46856de..8f44258e0 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
 	"name": "misskey",
 	"author": "syuilo <i@syuilo.com>",
-	"version": "0.0.3281",
+	"version": "0.0.3292",
 	"license": "MIT",
 	"description": "A miniblog-based SNS",
 	"bugs": "https://github.com/syuilo/misskey/issues",

From 23e4674966f87a2292f58c2c0a7fb0e8bbb7dcf9 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 9 Dec 2017 02:11:48 +0900
Subject: [PATCH 016/186] Fix bug

---
 src/web/app/mobile/tags/drive.tag        | 5 ++++-
 src/web/app/mobile/tags/drive/file.tag   | 3 ++-
 src/web/app/mobile/tags/drive/folder.tag | 5 +++--
 3 files changed, 9 insertions(+), 4 deletions(-)

diff --git a/src/web/app/mobile/tags/drive.tag b/src/web/app/mobile/tags/drive.tag
index a72c8d51c..41dbfddae 100644
--- a/src/web/app/mobile/tags/drive.tag
+++ b/src/web/app/mobile/tags/drive.tag
@@ -248,6 +248,7 @@
 		};
 
 		this.move = ev => {
+			ev.preventDefault();
 			this.cd(ev.item.folder);
 			return false;
 		};
@@ -333,7 +334,9 @@
 		this.prependFile = file => this.addFile(file, true);
 		this.prependFolder = file => this.addFolder(file, true);
 
-		this.goRoot = () => {
+		this.goRoot = ev => {
+			ev.preventDefault();
+
 			if (this.folder || this.file) {
 				this.update({
 					file: null,
diff --git a/src/web/app/mobile/tags/drive/file.tag b/src/web/app/mobile/tags/drive/file.tag
index 0b3506a43..196dd1141 100644
--- a/src/web/app/mobile/tags/drive/file.tag
+++ b/src/web/app/mobile/tags/drive/file.tag
@@ -138,7 +138,8 @@
 			this.isSelected = selections.some(f => f.id == this.file.id);
 		});
 
-		this.onclick = () => {
+		this.onclick = ev => {
+			ev.preventDefault();
 			this.browser.chooseFile(this.file);
 			return false;
 		};
diff --git a/src/web/app/mobile/tags/drive/folder.tag b/src/web/app/mobile/tags/drive/folder.tag
index 785847a9c..da55cf474 100644
--- a/src/web/app/mobile/tags/drive/folder.tag
+++ b/src/web/app/mobile/tags/drive/folder.tag
@@ -1,4 +1,4 @@
-<mk-drive-folder onclick={ onclick }>
+<mk-drive-folder>
 	<a onclick={ onclick } href="/i/drive/folder/{ folder.id }">
 		<div class="container">
 			<p class="name">%fa:folder%{ folder.name }</p>%fa:angle-right%
@@ -44,7 +44,8 @@
 		this.browser = this.parent;
 		this.folder = this.opts.folder;
 
-		this.onclick = () => {
+		this.onclick = ev => {
+			ev.preventDefault();
 			this.browser.cd(this.folder);
 			return false;
 		};

From 002a5417d85d83aa1627742b471f073903efacf1 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 9 Dec 2017 02:12:18 +0900
Subject: [PATCH 017/186] v3294

---
 CHANGELOG.md | 4 ++++
 package.json | 2 +-
 2 files changed, 5 insertions(+), 1 deletion(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 718c2d4ba..595666f68 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,10 @@ ChangeLog (Release Notes)
 =========================
 主に notable な changes を書いていきます
 
+3294 (2017/12/09)
+-----------------
+* バグ修正
+
 3292 (2017/12/09)
 -----------------
 * ユーザビリティの向上
diff --git a/package.json b/package.json
index 8f44258e0..4a9dde859 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
 	"name": "misskey",
 	"author": "syuilo <i@syuilo.com>",
-	"version": "0.0.3292",
+	"version": "0.0.3294",
 	"license": "MIT",
 	"description": "A miniblog-based SNS",
 	"bugs": "https://github.com/syuilo/misskey/issues",

From ce38cb3df83f3d4675323fdc4b4fbf83a62a87bc Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Fri, 8 Dec 2017 21:06:24 +0000
Subject: [PATCH 018/186] fix(package): update @fortawesome/fontawesome to
 version 1.0.1

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index 4a9dde859..ae76d52da 100644
--- a/package.json
+++ b/package.json
@@ -22,7 +22,7 @@
 		"format": "gulp format"
 	},
 	"dependencies": {
-		"@fortawesome/fontawesome": "1.0.0",
+		"@fortawesome/fontawesome": "1.0.1",
 		"@fortawesome/fontawesome-free-brands": "5.0.0",
 		"@fortawesome/fontawesome-free-regular": "5.0.0",
 		"@fortawesome/fontawesome-free-solid": "5.0.0",

From 58b893fd44a1657ba91c1ef29be4ab1dc0f88364 Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Fri, 8 Dec 2017 21:06:36 +0000
Subject: [PATCH 019/186] fix(package): update
 @fortawesome/fontawesome-free-solid to version 5.0.1

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index 4a9dde859..3a823ed9a 100644
--- a/package.json
+++ b/package.json
@@ -25,7 +25,7 @@
 		"@fortawesome/fontawesome": "1.0.0",
 		"@fortawesome/fontawesome-free-brands": "5.0.0",
 		"@fortawesome/fontawesome-free-regular": "5.0.0",
-		"@fortawesome/fontawesome-free-solid": "5.0.0",
+		"@fortawesome/fontawesome-free-solid": "5.0.1",
 		"@prezzemolo/rap": "0.1.2",
 		"@prezzemolo/zip": "0.0.3",
 		"@types/bcryptjs": "2.4.1",

From d475df37e0f74fb21927e3adc93bd437bdadb659 Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Fri, 8 Dec 2017 21:06:49 +0000
Subject: [PATCH 020/186] fix(package): update
 @fortawesome/fontawesome-free-regular to version 5.0.1

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index ae76d52da..c918a9555 100644
--- a/package.json
+++ b/package.json
@@ -24,7 +24,7 @@
 	"dependencies": {
 		"@fortawesome/fontawesome": "1.0.1",
 		"@fortawesome/fontawesome-free-brands": "5.0.0",
-		"@fortawesome/fontawesome-free-regular": "5.0.0",
+		"@fortawesome/fontawesome-free-regular": "5.0.1",
 		"@fortawesome/fontawesome-free-solid": "5.0.0",
 		"@prezzemolo/rap": "0.1.2",
 		"@prezzemolo/zip": "0.0.3",

From 4be641b357f25bf6116a9c04502eec5ceac347b4 Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Fri, 8 Dec 2017 21:07:00 +0000
Subject: [PATCH 021/186] fix(package): update
 @fortawesome/fontawesome-free-brands to version 5.0.1

---
 package.json | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/package.json b/package.json
index b4fb4b5f6..098677d63 100644
--- a/package.json
+++ b/package.json
@@ -23,9 +23,9 @@
 	},
 	"dependencies": {
 		"@fortawesome/fontawesome": "1.0.1",
-		"@fortawesome/fontawesome-free-brands": "5.0.0",
+		"@fortawesome/fontawesome-free-brands": "5.0.1",
 		"@fortawesome/fontawesome-free-regular": "5.0.0",
-		"@fortawesome/fontawesome-free-solid": "5.0.1",
+		"@fortawesome/fontawesome-free-solid": "5.0.0",
 		"@prezzemolo/rap": "0.1.2",
 		"@prezzemolo/zip": "0.0.3",
 		"@types/bcryptjs": "2.4.1",

From 66cb828b07bab7898443bd313c81a576a5071df0 Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Fri, 8 Dec 2017 23:23:29 +0000
Subject: [PATCH 022/186] fix(package): update pictograph to version 2.1.5

Closes #979
---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index 8d7b00532..31d2a71a8 100644
--- a/package.json
+++ b/package.json
@@ -132,7 +132,7 @@
 		"nprogress": "0.2.0",
 		"os-utils": "0.0.14",
 		"page": "1.7.1",
-		"pictograph": "2.1.2",
+		"pictograph": "2.1.5",
 		"prominence": "0.2.0",
 		"proxy-addr": "2.0.2",
 		"pug": "2.0.0-rc.4",

From b9ad38d56c5d1d05aff694bd05b08b26391dcb56 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 9 Dec 2017 20:23:58 +0900
Subject: [PATCH 023/186] :art:

---
 locales/en.yml                              |   1 +
 locales/ja.yml                              |   1 +
 src/web/app/common/tags/authorized-apps.tag |   4 +-
 src/web/app/common/tags/signin-history.tag  | 120 ++++++++-----
 src/web/app/desktop/style.styl              |  65 +------
 src/web/app/desktop/tags/settings.tag       | 178 ++++++++------------
 src/web/app/desktop/ui.styl                 | 119 +++++++++++++
 7 files changed, 274 insertions(+), 214 deletions(-)
 create mode 100644 src/web/app/desktop/ui.styl

diff --git a/locales/en.yml b/locales/en.yml
index 020fc3949..16f68ab63 100644
--- a/locales/en.yml
+++ b/locales/en.yml
@@ -342,6 +342,7 @@ desktop:
       next: "Next post"
 
     mk-settings:
+      profile: "Profile"
       security: "Security"
       password: "Password"
       2fa: "Two-factor authentication"
diff --git a/locales/ja.yml b/locales/ja.yml
index c5996c76b..f50e50a5c 100644
--- a/locales/ja.yml
+++ b/locales/ja.yml
@@ -342,6 +342,7 @@ desktop:
       next: "次の投稿"
 
     mk-settings:
+      profile: "プロフィール"
       security: "セキュリティ"
       password: "パスワード"
       2fa: "二段階認証"
diff --git a/src/web/app/common/tags/authorized-apps.tag b/src/web/app/common/tags/authorized-apps.tag
index 0078a1863..3f3714332 100644
--- a/src/web/app/common/tags/authorized-apps.tag
+++ b/src/web/app/common/tags/authorized-apps.tag
@@ -1,5 +1,7 @@
 <mk-authorized-apps>
-	<p class="none" if={ !fetching && apps.length == 0 }>%i18n:common.tags.mk-authorized-apps.no-apps%</p>
+	<div class="none ui info" if={ !fetching && apps.length == 0 }>
+		<p>%i18n:common.tags.mk-authorized-apps.no-apps%</p>
+	</div>
 	<div class="apps" if={ apps.length != 0 }>
 		<div each={ app in apps }>
 			<p><b>{ app.name }</b></p>
diff --git a/src/web/app/common/tags/signin-history.tag b/src/web/app/common/tags/signin-history.tag
index 03afd7232..cdd58c4c6 100644
--- a/src/web/app/common/tags/signin-history.tag
+++ b/src/web/app/common/tags/signin-history.tag
@@ -1,55 +1,11 @@
 <mk-signin-history>
 	<div class="records" if={ history.length != 0 }>
-		<div each={ history }>
-			<mk-time time={ created_at }/>
-			<header>
-				<virtual if={ success }>%fa:check%</virtual>
-				<virtual if={ !success }>%fa:times%</virtual>
-				<span class="ip">{ ip }</span>
-			</header>
-			<pre><code>{ JSON.stringify(headers, null, '    ') }</code></pre>
-		</div>
+		<mk-signin-record each={ rec in history } rec={ rec }/>
 	</div>
 	<style>
 		:scope
 			display block
 
-			> .records
-				> div
-					padding 16px 0 0 0
-					border-bottom solid 1px #eee
-
-					> header
-
-						> [data-fa]
-							margin-right 8px
-
-							&.check
-								color #0fda82
-
-							&.times
-								color #ff3100
-
-						> .ip
-							display inline-block
-							color #444
-							background #f8f8f8
-
-					> mk-time
-						position absolute
-						top 16px
-						right 0
-						color #777
-
-					> pre
-						overflow auto
-						max-height 100px
-
-						> code
-							white-space pre-wrap
-							word-break break-all
-							color #4a535a
-
 	</style>
 	<script>
 		this.mixin('i');
@@ -84,3 +40,77 @@
 		};
 	</script>
 </mk-signin-history>
+
+<mk-signin-record>
+	<header onclick={ toggle }>
+		<virtual if={ rec.success }>%fa:check%</virtual>
+		<virtual if={ !rec.success }>%fa:times%</virtual>
+		<span class="ip">{ rec.ip }</span>
+		<mk-time time={ rec.created_at }/>
+	</header>
+	<pre ref="headers" class="json" show={ show }>{ JSON.stringify(rec.headers, null, 2) }</pre>
+
+	<style>
+		:scope
+			display block
+			border-bottom solid 1px #eee
+
+			> header
+				display flex
+				padding 8px 0
+				line-height 32px
+				cursor pointer
+
+				> [data-fa]
+					margin-right 8px
+					text-align left
+
+					&.check
+						color #0fda82
+
+					&.times
+						color #ff3100
+
+				> .ip
+					display inline-block
+					text-align left
+					padding 8px
+					line-height 16px
+					font-family monospace
+					font-size 14px
+					color #444
+					background #f8f8f8
+					border-radius 4px
+
+				> mk-time
+					margin-left auto
+					text-align right
+					color #777
+
+			> pre
+				overflow auto
+				margin 0 0 16px 0
+				max-height 100px
+				white-space pre-wrap
+				word-break break-all
+				color #4a535a
+
+	</style>
+
+	<script>
+		import hljs from 'highlight.js';
+
+		this.rec = this.opts.rec;
+		this.show = false;
+
+		this.on('mount', () => {
+			hljs.highlightBlock(this.refs.headers);
+		});
+
+		this.toggle = () => {
+			this.update({
+				show: !this.show
+			});
+		};
+	</script>
+</mk-signin-record>
diff --git a/src/web/app/desktop/style.styl b/src/web/app/desktop/style.styl
index d99e5df2b..c893e2ed6 100644
--- a/src/web/app/desktop/style.styl
+++ b/src/web/app/desktop/style.styl
@@ -2,6 +2,8 @@
 @import "../reset"
 @import "../../../../node_modules/cropperjs/dist/cropper.css"
 
+@import "./ui"
+
 *::input-placeholder
 	color #D8CBC5
 
@@ -47,66 +49,3 @@ html
 		#wait
 			right auto
 			left 15px
-
-button
-	font-family sans-serif
-
-	*
-		pointer-events none
-
-	&.style-normal
-	&.style-primary
-		display block
-		cursor pointer
-		padding 0 16px
-		margin 0
-		min-width 100px
-		height 40px
-		font-size 1em
-		outline none
-		border-radius 4px
-
-		&:focus
-			&:after
-				content ""
-				pointer-events none
-				position absolute
-				top -5px
-				right -5px
-				bottom -5px
-				left -5px
-				border 2px solid rgba($theme-color, 0.3)
-				border-radius 8px
-
-		&:disabled
-			opacity 0.7
-			cursor default
-
-	&.style-normal
-		color #888
-		background linear-gradient(to bottom, #ffffff 0%, #f5f5f5 100%)
-		border solid 1px #e2e2e2
-
-		&:hover
-			background linear-gradient(to bottom, #f9f9f9 0%, #ececec 100%)
-			border-color #dcdcdc
-
-		&:active
-			background #ececec
-			border-color #dcdcdc
-
-	&.style-primary
-		color $theme-color-foreground
-		background linear-gradient(to bottom, lighten($theme-color, 25%) 0%, lighten($theme-color, 10%) 100%)
-		border solid 1px lighten($theme-color, 15%)
-
-		&:not(:disabled)
-			font-weight bold
-
-		&:hover:not(:disabled)
-			background linear-gradient(to bottom, lighten($theme-color, 8%) 0%, darken($theme-color, 8%) 100%)
-			border-color $theme-color
-
-		&:active:not(:disabled)
-			background $theme-color
-			border-color $theme-color
diff --git a/src/web/app/desktop/tags/settings.tag b/src/web/app/desktop/tags/settings.tag
index 2c38f1352..f44cef850 100644
--- a/src/web/app/desktop/tags/settings.tag
+++ b/src/web/app/desktop/tags/settings.tag
@@ -1,47 +1,23 @@
 <mk-settings>
 	<div class="nav">
-		<p class={ active: page == 'account' } onmousedown={ setPage.bind(null, 'account') }>%fa:user .fw%アカウント</p>
+		<p class={ active: page == 'profile' } onmousedown={ setPage.bind(null, 'profile') }>%fa:user .fw%%i18n:desktop.tags.mk-settings.profile%</p>
 		<p class={ active: page == 'web' } onmousedown={ setPage.bind(null, 'web') }>%fa:desktop .fw%Web</p>
 		<p class={ active: page == 'notification' } onmousedown={ setPage.bind(null, 'notification') }>%fa:R bell .fw%通知</p>
 		<p class={ active: page == 'drive' } onmousedown={ setPage.bind(null, 'drive') }>%fa:cloud .fw%ドライブ</p>
 		<p class={ active: page == 'apps' } onmousedown={ setPage.bind(null, 'apps') }>%fa:puzzle-piece .fw%アプリ</p>
 		<p class={ active: page == 'twitter' } onmousedown={ setPage.bind(null, 'twitter') }>%fa:B twitter .fw%Twitter</p>
-		<p class={ active: page == 'signin' } onmousedown={ setPage.bind(null, 'signin') }>%fa:sign-in-alt .fw%ログイン履歴</p>
 		<p class={ active: page == 'security' } onmousedown={ setPage.bind(null, 'security') }>%fa:unlock-alt .fw%%i18n:desktop.tags.mk-settings.security%</p>
 		<p class={ active: page == 'api' } onmousedown={ setPage.bind(null, 'api') }>%fa:key .fw%API</p>
 	</div>
 	<div class="pages">
-		<section class="account" show={ page == 'account' }>
-			<h1>アカウント</h1>
-			<label class="avatar">
-				<p>アバター</p><img class="avatar" src={ I.avatar_url + '?thumbnail&size=64' } alt="avatar"/>
-				<button class="style-normal" onclick={ avatar }>画像を選択</button>
-			</label>
-			<label>
-				<p>名前</p>
-				<input ref="accountName" type="text" value={ I.name }/>
-			</label>
-			<label>
-				<p>場所</p>
-				<input ref="accountLocation" type="text" value={ I.profile.location }/>
-			</label>
-			<label>
-				<p>自己紹介</p>
-				<textarea ref="accountDescription">{ I.description }</textarea>
-			</label>
-			<label>
-				<p>誕生日</p>
-				<input ref="accountBirthday" type="date" value={ I.profile.birthday }/>
-			</label>
-			<button class="style-primary" onclick={ updateAccount }>保存</button>
+		<section class="profile" show={ page == 'profile' }>
+			<h1>%i18n:desktop.tags.mk-settings.profile%</h1>
+			<mk-profile-setting/>
 		</section>
 
 		<section class="web" show={ page == 'web' }>
 			<h1>デザイン</h1>
-			<a href="/i/customize-home">ホームをカスタマイズ</a>
-		</section>
-
-		<section class="web" show={ page == 'web' }>
+			<a href="/i/customize-home" class="ui button">ホームをカスタマイズ</a>
 		</section>
 
 		<section class="apps" show={ page == 'apps' }>
@@ -54,11 +30,6 @@
 			<mk-twitter-setting/>
 		</section>
 
-		<section class="signin" show={ page == 'signin' }>
-			<h1>ログイン履歴</h1>
-			<mk-signin-history/>
-		</section>
-
 		<section class="password" show={ page == 'security' }>
 			<h1>%i18n:desktop.tags.mk-settings.password%</h1>
 			<mk-password-setting/>
@@ -69,6 +40,11 @@
 			<mk-2fa-setting/>
 		</section>
 
+		<section class="signin" show={ page == 'security' }>
+			<h1>サインイン履歴</h1>
+			<mk-signin-history/>
+		</section>
+
 		<section class="api" show={ page == 'api' }>
 			<h1>API</h1>
 			<mk-api-info/>
@@ -80,25 +56,6 @@
 			width 100%
 			height 100%
 
-			input:not([type])
-			input[type='text']
-			input[type='password']
-			input[type='email']
-			input[type='date']
-			textarea
-				padding 8px
-				width 100%
-				font-size 16px
-				color #55595c
-				border solid 1px #dadada
-				border-radius 4px
-
-				&:hover
-					border-color #aeaeae
-
-				&:focus
-					border-color #aeaeae
-
 			> .nav
 				flex 0 0 200px
 				width 100%
@@ -133,64 +90,80 @@
 				overflow auto
 
 				> section
-					padding 32px
-
-					//	& + section
-					//		margin-top 16px
+					margin 32px
 
 					h1
 						display block
-						margin 0
+						margin 0 0 1em 0
 						padding 0 0 8px 0
 						font-size 1em
 						color #555
 						border-bottom solid 1px #eee
 
-					label
-						display block
-						margin 16px 0
+					label.checkbox
+						> input
+							position absolute
+							top 0
+							left 0
 
-						&:after
-							content ""
-							display block
-							clear both
+							&:checked + p
+								color $theme-color
 
 						> p
-							margin 0 0 8px 0
+							width calc(100% - 32px)
+							margin 0 0 0 32px
 							font-weight bold
-							color #373a3c
 
-						&.checkbox
-							> input
-								position absolute
-								top 0
-								left 0
+							&:last-child
+								font-weight normal
+								color #999
 
-								&:checked + p
-									color $theme-color
+	</style>
+	<script>
+		this.page = 'profile';
 
-							> p
-								width calc(100% - 32px)
-								margin 0 0 0 32px
-								font-weight bold
+		this.setPage = page => {
+			this.page = page;
+		};
+	</script>
+</mk-settings>
 
-								&:last-child
-									font-weight normal
-									color #999
+<mk-profile-setting>
+	<label class="avatar ui from group">
+		<p>アバター</p><img class="avatar" src={ I.avatar_url + '?thumbnail&size=64' } alt="avatar"/>
+		<button class="ui" onclick={ avatar }>画像を選択</button>
+	</label>
+	<label class="ui from group">
+		<p>名前</p>
+		<input ref="accountName" type="text" value={ I.name } class="ui"/>
+	</label>
+	<label class="ui from group">
+		<p>場所</p>
+		<input ref="accountLocation" type="text" value={ I.profile.location } class="ui"/>
+	</label>
+	<label class="ui from group">
+		<p>自己紹介</p>
+		<textarea ref="accountDescription" class="ui">{ I.description }</textarea>
+	</label>
+	<label class="ui from group">
+		<p>誕生日</p>
+		<input ref="accountBirthday" type="date" value={ I.profile.birthday } class="ui"/>
+	</label>
+	<button class="ui primary" onclick={ updateAccount }>保存</button>
+	<style>
+		:scope
+			display block
 
-					&.account
-						> .general
-							> .avatar
-								> img
-									display block
-									float left
-									width 64px
-									height 64px
-									border-radius 4px
+			> .avatar
+				> img
+					display inline-block
+					vertical-align top
+					width 64px
+					height 64px
+					border-radius 4px
 
-								> button
-									float left
-									margin-left 8px
+				> button
+					margin-left 8px
 
 	</style>
 	<script>
@@ -200,12 +173,6 @@
 		this.mixin('i');
 		this.mixin('api');
 
-		this.page = 'account';
-
-		this.setPage = page => {
-			this.page = page;
-		};
-
 		this.avatar = () => {
 			updateAvatar(this.I);
 		};
@@ -221,7 +188,7 @@
 			});
 		};
 	</script>
-</mk-settings>
+</mk-profile-setting>
 
 <mk-api-info>
 	<p>Token:<code>{ I.token }</code></p>
@@ -254,7 +221,7 @@
 </mk-api-info>
 
 <mk-password-setting>
-	<button onclick={ reset }>%i18n:desktop.tags.mk-password-setting.reset%</button>
+	<button onclick={ reset } class="ui primary">%i18n:desktop.tags.mk-password-setting.reset%</button>
 	<style>
 		:scope
 			display block
@@ -293,17 +260,18 @@
 
 <mk-2fa-setting>
 	<p>%i18n:desktop.tags.mk-2fa-setting.intro%</p>
-	<p>%i18n:desktop.tags.mk-2fa-setting.caution%</p>
-	<p><button onclick={ register }>%i18n:desktop.tags.mk-2fa-setting.register%</button></p>
+	<div class="ui info warn"><p>%i18n:desktop.tags.mk-2fa-setting.caution%</p></div>
+	<p if={ !data }><button onclick={ register } class="ui primary">%i18n:desktop.tags.mk-2fa-setting.register%</button></p>
 	<div if={ data }>
 		<ol>
 			<li>%i18n:desktop.tags.mk-2fa-setting.authenticator% <a href="https://support.google.com/accounts/answer/1066447" target="_blank">%i18n:desktop.tags.mk-2fa-setting.howtoinstall%</a></li>
 			<li>%i18n:desktop.tags.mk-2fa-setting.scan%<br><img src={ data.qr }></li>
 			<li>%i18n:desktop.tags.mk-2fa-setting.done%<br>
-				<input type="number" ref="token"><button onclick={ submit }>%i18n:desktop.tags.mk-2fa-setting.submit%</button>
+				<input type="number" ref="token" class="ui">
+				<button onclick={ submit } class="ui primary">%i18n:desktop.tags.mk-2fa-setting.submit%</button>
 			</li>
-			<li>%i18n:desktop.tags.mk-2fa-setting.info%</li>
 		</ol>
+		<div class="ui info"><p>%i18n:desktop.tags.mk-2fa-setting.info%</p></div>
 	</div>
 	<style>
 		:scope
diff --git a/src/web/app/desktop/ui.styl b/src/web/app/desktop/ui.styl
new file mode 100644
index 000000000..a98bfb419
--- /dev/null
+++ b/src/web/app/desktop/ui.styl
@@ -0,0 +1,119 @@
+@import "../app"
+
+button
+	font-family sans-serif
+
+	*
+		pointer-events none
+
+button.ui
+.button.ui
+	display inline-block
+	cursor pointer
+	padding 0 14px
+	margin 0
+	min-width 100px
+	line-height 38px
+	font-size 14px
+	color #888
+	text-decoration none
+	background linear-gradient(to bottom, #ffffff 0%, #f5f5f5 100%)
+	border solid 1px #e2e2e2
+	border-radius 4px
+	outline none
+
+	&:focus
+		&:after
+			content ""
+			pointer-events none
+			position absolute
+			top -5px
+			right -5px
+			bottom -5px
+			left -5px
+			border 2px solid rgba($theme-color, 0.3)
+			border-radius 8px
+
+	&:disabled
+		opacity 0.7
+		cursor default
+
+	&:hover
+		background linear-gradient(to bottom, #f9f9f9 0%, #ececec 100%)
+		border-color #dcdcdc
+
+	&:active
+		background #ececec
+		border-color #dcdcdc
+
+	&.primary
+		color $theme-color-foreground
+		background linear-gradient(to bottom, lighten($theme-color, 25%) 0%, lighten($theme-color, 10%) 100%)
+		border solid 1px lighten($theme-color, 15%)
+
+		&:not(:disabled)
+			font-weight bold
+
+		&:hover:not(:disabled)
+			background linear-gradient(to bottom, lighten($theme-color, 8%) 0%, darken($theme-color, 8%) 100%)
+			border-color $theme-color
+
+		&:active:not(:disabled)
+			background $theme-color
+			border-color $theme-color
+
+input:not([type]).ui
+input[type='text'].ui
+input[type='password'].ui
+input[type='email'].ui
+input[type='date'].ui
+input[type='number'].ui
+textarea.ui
+	display block
+	padding 10px
+	width 100%
+	height 40px
+	font-family sans-serif
+	font-size 16px
+	color #55595c
+	border solid 1px #dadada
+	border-radius 4px
+
+	&:hover
+		border-color #b0b0b0
+
+	&:focus
+		border-color $theme-color
+
+textarea.ui
+	min-width 100%
+	max-width 100%
+	min-height 64px
+
+.ui.info
+	display block
+	margin 1em 0
+	padding 0 1em
+	font-size 90%
+	color rgba(#000, 0.87)
+	background #f8f8f9
+	border solid 1px rgba(34, 36, 38, 0.22)
+	border-radius 4px
+
+	> p
+		opacity 0.8
+
+	&.warn
+		color #573a08
+		background #FFFAF3
+		border-color #C9BA9B
+
+.ui.from.group
+	display block
+	margin 16px 0
+
+	> p:first-child
+		margin 0 0 6px 0
+		font-size 90%
+		font-weight bold
+		color rgba(#373a3c, 0.9)

From 0a29dd598afa34e1027fd438dfe91c49f249763a Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 9 Dec 2017 20:31:33 +0900
Subject: [PATCH 024/186] :art:

---
 src/web/app/common/tags/authorized-apps.tag | 2 +-
 src/web/app/desktop/tags/settings.tag       | 4 ++--
 src/web/app/desktop/ui.styl                 | 3 +++
 3 files changed, 6 insertions(+), 3 deletions(-)

diff --git a/src/web/app/common/tags/authorized-apps.tag b/src/web/app/common/tags/authorized-apps.tag
index 3f3714332..0594032de 100644
--- a/src/web/app/common/tags/authorized-apps.tag
+++ b/src/web/app/common/tags/authorized-apps.tag
@@ -1,6 +1,6 @@
 <mk-authorized-apps>
 	<div class="none ui info" if={ !fetching && apps.length == 0 }>
-		<p>%i18n:common.tags.mk-authorized-apps.no-apps%</p>
+		<p>%fa:info-circle%%i18n:common.tags.mk-authorized-apps.no-apps%</p>
 	</div>
 	<div class="apps" if={ apps.length != 0 }>
 		<div each={ app in apps }>
diff --git a/src/web/app/desktop/tags/settings.tag b/src/web/app/desktop/tags/settings.tag
index f44cef850..b6e50cdb4 100644
--- a/src/web/app/desktop/tags/settings.tag
+++ b/src/web/app/desktop/tags/settings.tag
@@ -260,7 +260,7 @@
 
 <mk-2fa-setting>
 	<p>%i18n:desktop.tags.mk-2fa-setting.intro%</p>
-	<div class="ui info warn"><p>%i18n:desktop.tags.mk-2fa-setting.caution%</p></div>
+	<div class="ui info warn"><p>%fa:exclamation-triangle%%i18n:desktop.tags.mk-2fa-setting.caution%</p></div>
 	<p if={ !data }><button onclick={ register } class="ui primary">%i18n:desktop.tags.mk-2fa-setting.register%</button></p>
 	<div if={ data }>
 		<ol>
@@ -271,7 +271,7 @@
 				<button onclick={ submit } class="ui primary">%i18n:desktop.tags.mk-2fa-setting.submit%</button>
 			</li>
 		</ol>
-		<div class="ui info"><p>%i18n:desktop.tags.mk-2fa-setting.info%</p></div>
+		<div class="ui info"><p>%fa:info-circle%%i18n:desktop.tags.mk-2fa-setting.info%</p></div>
 	</div>
 	<style>
 		:scope
diff --git a/src/web/app/desktop/ui.styl b/src/web/app/desktop/ui.styl
index a98bfb419..cb98bf06a 100644
--- a/src/web/app/desktop/ui.styl
+++ b/src/web/app/desktop/ui.styl
@@ -103,6 +103,9 @@ textarea.ui
 	> p
 		opacity 0.8
 
+		> [data-fa]:first-child
+			margin-right 0.25em
+
 	&.warn
 		color #573a08
 		background #FFFAF3

From 510eba466ced695bdb20582f13f2df4d8b170090 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 9 Dec 2017 20:33:07 +0900
Subject: [PATCH 025/186] v3308

---
 CHANGELOG.md | 4 ++++
 package.json | 2 +-
 2 files changed, 5 insertions(+), 1 deletion(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 595666f68..5707a7862 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,10 @@ ChangeLog (Release Notes)
 =========================
 主に notable な changes を書いていきます
 
+3308 (2017/12/09)
+-----------------
+* :art:
+
 3294 (2017/12/09)
 -----------------
 * バグ修正
diff --git a/package.json b/package.json
index 8d7b00532..ed2c80fc6 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
 	"name": "misskey",
 	"author": "syuilo <i@syuilo.com>",
-	"version": "0.0.3294",
+	"version": "0.0.3308",
 	"license": "MIT",
 	"description": "A miniblog-based SNS",
 	"bugs": "https://github.com/syuilo/misskey/issues",

From 7792192848ca3442474377403fc99e4e94709750 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 9 Dec 2017 20:45:08 +0900
Subject: [PATCH 026/186] :v:

---
 locales/en.yml                        | 13 ++++++++++++-
 locales/ja.yml                        | 11 +++++++++++
 src/web/app/desktop/tags/settings.tag | 16 ++++++++--------
 3 files changed, 31 insertions(+), 9 deletions(-)

diff --git a/locales/en.yml b/locales/en.yml
index 16f68ab63..b1fe4a50b 100644
--- a/locales/en.yml
+++ b/locales/en.yml
@@ -288,6 +288,15 @@ desktop:
     mk-ui-header-notifications:
       title: "Notifications"
 
+    mk-profile-setting:
+      avatar: "Avatar"
+      choice-avatar: "Choice an image"
+      name: "Name"
+      location: "Location"
+      description: "Description"
+      birthday: "Birthday"
+      save: "Update profile"
+
     mk-password-setting:
       reset: "Change your password"
       enter-current-password: "Enter the current password"
@@ -297,7 +306,9 @@ desktop:
       changed: "Password updated successfully"
 
     mk-2fa-setting:
-      intro: "If you set up 2-step verification, you will need not only a password at sign-in but also a pre-registered physical device (such as your smartphone), which will improve security."
+      intro: "If you set up 2-step verification, you will need not only a password at sign-in but also a pre-registered physical device (such as your smartphone), which will improve security. "
+      detail: "See details..."
+      url: "https://www.google.com/landing/2step/"
       caution: "As a caveat, security improves, but you can not sign in to Misskey if you lose a registered device, etc."
       register: "Register a device"
       enter-password: "Enter the password"
diff --git a/locales/ja.yml b/locales/ja.yml
index f50e50a5c..12f5580e2 100644
--- a/locales/ja.yml
+++ b/locales/ja.yml
@@ -288,6 +288,15 @@ desktop:
     mk-ui-header-notifications:
       title: "通知"
 
+    mk-profile-setting:
+      avatar: "アバター"
+      choice-avatar: "画像を選択"
+      name: "名前"
+      location: "場所"
+      description: "自己紹介"
+      birthday: "誕生日"
+      save: "保存"
+
     mk-password-setting:
       reset: "パスワードを変更する"
       enter-current-password: "現在のパスワードを入力してください"
@@ -298,6 +307,8 @@ desktop:
 
     mk-2fa-setting:
       intro: "二段階認証を設定すると、サインイン時にパスワードだけでなく、予め登録しておいた物理的なデバイス(例えばあなたのスマートフォンなど)も必要になり、よりセキュリティが向上します。"
+      detail: "詳細..."
+      url: "https://www.google.co.jp/intl/ja/landing/2step/"
       caution: "登録したデバイスを紛失するなどした場合、Misskeyにサインインできなくなりますのでご注意ください。"
       register: "デバイスを登録する"
       enter-password: "パスワードを入力してください"
diff --git a/src/web/app/desktop/tags/settings.tag b/src/web/app/desktop/tags/settings.tag
index b6e50cdb4..63458baa2 100644
--- a/src/web/app/desktop/tags/settings.tag
+++ b/src/web/app/desktop/tags/settings.tag
@@ -130,26 +130,26 @@
 
 <mk-profile-setting>
 	<label class="avatar ui from group">
-		<p>アバター</p><img class="avatar" src={ I.avatar_url + '?thumbnail&size=64' } alt="avatar"/>
-		<button class="ui" onclick={ avatar }>画像を選択</button>
+		<p>%i18n:desktop.tags.mk-profile-setting.avatar%</p><img class="avatar" src={ I.avatar_url + '?thumbnail&size=64' } alt="avatar"/>
+		<button class="ui" onclick={ avatar }>%i18n:desktop.tags.mk-profile-setting.choice-avatar%</button>
 	</label>
 	<label class="ui from group">
-		<p>名前</p>
+		<p>%i18n:desktop.tags.mk-profile-setting.name%</p>
 		<input ref="accountName" type="text" value={ I.name } class="ui"/>
 	</label>
 	<label class="ui from group">
-		<p>場所</p>
+		<p>%i18n:desktop.tags.mk-profile-setting.location%</p>
 		<input ref="accountLocation" type="text" value={ I.profile.location } class="ui"/>
 	</label>
 	<label class="ui from group">
-		<p>自己紹介</p>
+		<p>%i18n:desktop.tags.mk-profile-setting.description%</p>
 		<textarea ref="accountDescription" class="ui">{ I.description }</textarea>
 	</label>
 	<label class="ui from group">
-		<p>誕生日</p>
+		<p>%i18n:desktop.tags.mk-profile-setting.birthday%</p>
 		<input ref="accountBirthday" type="date" value={ I.profile.birthday } class="ui"/>
 	</label>
-	<button class="ui primary" onclick={ updateAccount }>保存</button>
+	<button class="ui primary" onclick={ updateAccount }>%i18n:desktop.tags.mk-profile-setting.save%</button>
 	<style>
 		:scope
 			display block
@@ -259,7 +259,7 @@
 </mk-password-setting>
 
 <mk-2fa-setting>
-	<p>%i18n:desktop.tags.mk-2fa-setting.intro%</p>
+	<p>%i18n:desktop.tags.mk-2fa-setting.intro%<a href="%i18n:desktop.tags.mk-2fa-setting.url%" target="_blank">%i18n:desktop.tags.mk-2fa-setting.detail%</a></p>
 	<div class="ui info warn"><p>%fa:exclamation-triangle%%i18n:desktop.tags.mk-2fa-setting.caution%</p></div>
 	<p if={ !data }><button onclick={ register } class="ui primary">%i18n:desktop.tags.mk-2fa-setting.register%</button></p>
 	<div if={ data }>

From c420901c24bcc2204d164b43f246dd392033eb90 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 9 Dec 2017 20:45:31 +0900
Subject: [PATCH 027/186] v3310

---
 CHANGELOG.md | 4 ++++
 package.json | 2 +-
 2 files changed, 5 insertions(+), 1 deletion(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5707a7862..6d0550b88 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,10 @@ ChangeLog (Release Notes)
 =========================
 主に notable な changes を書いていきます
 
+3310 (2017/12/09)
+-----------------
+* i18nなど
+
 3308 (2017/12/09)
 -----------------
 * :art:
diff --git a/package.json b/package.json
index ed2c80fc6..3ff982d67 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
 	"name": "misskey",
 	"author": "syuilo <i@syuilo.com>",
-	"version": "0.0.3308",
+	"version": "0.0.3310",
 	"license": "MIT",
 	"description": "A miniblog-based SNS",
 	"bugs": "https://github.com/syuilo/misskey/issues",

From 37edacce44cb7d63c35fee88e0b43028a1c7765c Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 9 Dec 2017 22:35:26 +0900
Subject: [PATCH 028/186] Use ImageMagick instead of GraphicsMagick

---
 docs/setup.en.md                    | 2 +-
 docs/setup.ja.md                    | 2 +-
 src/api/common/add-file-to-drive.ts | 6 +++++-
 src/file/server.ts                  | 7 ++++++-
 src/utils/dependencyInfo.ts         | 2 +-
 5 files changed, 14 insertions(+), 5 deletions(-)

diff --git a/docs/setup.en.md b/docs/setup.en.md
index 9c31e4f17..b81245d89 100644
--- a/docs/setup.en.md
+++ b/docs/setup.en.md
@@ -53,7 +53,7 @@ Please install and setup these softwares:
 * *Node.js* and *npm*
 * **[MongoDB](https://www.mongodb.com/)**
 * **[Redis](https://redis.io/)**
-* **[GraphicsMagick](http://www.graphicsmagick.org/)**
+* **[ImageMagick](http://www.imagemagick.org/script/index.php)**
 
 ##### Optional
 * [Elasticsearch](https://www.elastic.co/) - used to provide searching feature instead of MongoDB
diff --git a/docs/setup.ja.md b/docs/setup.ja.md
index 1e8bb553f..1662d1ee5 100644
--- a/docs/setup.ja.md
+++ b/docs/setup.ja.md
@@ -54,7 +54,7 @@ web-push generate-vapid-keys
 * *Node.js* と *npm*
 * **[MongoDB](https://www.mongodb.com/)**
 * **[Redis](https://redis.io/)**
-* **[GraphicsMagick](http://www.graphicsmagick.org/)**
+* **[ImageMagick](http://www.imagemagick.org/script/index.php)**
 
 ##### オプション
 * [Elasticsearch](https://www.elastic.co/) - 検索機能を向上させるために用います。
diff --git a/src/api/common/add-file-to-drive.ts b/src/api/common/add-file-to-drive.ts
index dea02eeca..109e88610 100644
--- a/src/api/common/add-file-to-drive.ts
+++ b/src/api/common/add-file-to-drive.ts
@@ -5,7 +5,7 @@ import * as stream from 'stream';
 
 import * as mongodb from 'mongodb';
 import * as crypto from 'crypto';
-import * as gm from 'gm';
+import * as _gm from 'gm';
 import * as debug from 'debug';
 import fileType = require('file-type');
 import prominence = require('prominence');
@@ -16,6 +16,10 @@ import serialize from '../serializers/drive-file';
 import event, { publishDriveStream } from '../event';
 import config from '../../conf';
 
+const gm = _gm.subClass({
+	imageMagick: true
+});
+
 const log = debug('misskey:register-drive-file');
 
 const tmpFile = (): Promise<string> => new Promise((resolve, reject) => {
diff --git a/src/file/server.ts b/src/file/server.ts
index 1f8d21b80..187ce75c2 100644
--- a/src/file/server.ts
+++ b/src/file/server.ts
@@ -7,11 +7,15 @@ import * as express from 'express';
 import * as bodyParser from 'body-parser';
 import * as cors from 'cors';
 import * as mongodb from 'mongodb';
-import * as gm from 'gm';
+import * as _gm from 'gm';
 import * as stream from 'stream';
 
 import DriveFile, { getGridFSBucket } from '../api/models/drive-file';
 
+const gm = _gm.subClass({
+	imageMagick: true
+});
+
 /**
  * Init app
  */
@@ -78,6 +82,7 @@ function thumbnail(data: stream.Readable, type: string, resize: number): ISend {
 	const stream = g
 		.compress('jpeg')
 		.quality(80)
+		.noProfile() // Remove EXIF
 		.stream();
 
 	return {
diff --git a/src/utils/dependencyInfo.ts b/src/utils/dependencyInfo.ts
index 818fa3136..89af0d20f 100644
--- a/src/utils/dependencyInfo.ts
+++ b/src/utils/dependencyInfo.ts
@@ -11,7 +11,7 @@ export default class {
 	public showAll(): void {
 		this.show('MongoDB', 'mongo --version', x => x.match(/^MongoDB shell version:? (.*)\r?\n/));
 		this.show('Redis', 'redis-server --version', x => x.match(/v=([0-9\.]*)/));
-		this.show('GraphicsMagick', 'gm -version', x => x.match(/^GraphicsMagick ([0-9\.]*) .*/));
+		this.show('ImageMagick', 'magick -version', x => x.match(/^Version: ImageMagick (.+?)\r?\n/));
 	}
 
 	public show(serviceName: string, command: string, transform: (x: string) => RegExpMatchArray): void {

From 42aef0abe0a98e46acb0e9278dbd4347b79a7b53 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 9 Dec 2017 23:03:34 +0900
Subject: [PATCH 029/186] :v:

---
 locales/en.yml                        |  1 +
 locales/ja.yml                        |  1 +
 src/web/app/desktop/tags/settings.tag | 68 ++++++++++++++++++++++++++-
 3 files changed, 69 insertions(+), 1 deletion(-)

diff --git a/locales/en.yml b/locales/en.yml
index b1fe4a50b..4dadcb806 100644
--- a/locales/en.yml
+++ b/locales/en.yml
@@ -354,6 +354,7 @@ desktop:
 
     mk-settings:
       profile: "Profile"
+      drive: "Drive"
       security: "Security"
       password: "Password"
       2fa: "Two-factor authentication"
diff --git a/locales/ja.yml b/locales/ja.yml
index 12f5580e2..d38ae682e 100644
--- a/locales/ja.yml
+++ b/locales/ja.yml
@@ -354,6 +354,7 @@ desktop:
 
     mk-settings:
       profile: "プロフィール"
+      drive: "ドライブ"
       security: "セキュリティ"
       password: "パスワード"
       2fa: "二段階認証"
diff --git a/src/web/app/desktop/tags/settings.tag b/src/web/app/desktop/tags/settings.tag
index 63458baa2..9f71da687 100644
--- a/src/web/app/desktop/tags/settings.tag
+++ b/src/web/app/desktop/tags/settings.tag
@@ -3,7 +3,7 @@
 		<p class={ active: page == 'profile' } onmousedown={ setPage.bind(null, 'profile') }>%fa:user .fw%%i18n:desktop.tags.mk-settings.profile%</p>
 		<p class={ active: page == 'web' } onmousedown={ setPage.bind(null, 'web') }>%fa:desktop .fw%Web</p>
 		<p class={ active: page == 'notification' } onmousedown={ setPage.bind(null, 'notification') }>%fa:R bell .fw%通知</p>
-		<p class={ active: page == 'drive' } onmousedown={ setPage.bind(null, 'drive') }>%fa:cloud .fw%ドライブ</p>
+		<p class={ active: page == 'drive' } onmousedown={ setPage.bind(null, 'drive') }>%fa:cloud .fw%%i18n:desktop.tags.mk-settings.drive%</p>
 		<p class={ active: page == 'apps' } onmousedown={ setPage.bind(null, 'apps') }>%fa:puzzle-piece .fw%アプリ</p>
 		<p class={ active: page == 'twitter' } onmousedown={ setPage.bind(null, 'twitter') }>%fa:B twitter .fw%Twitter</p>
 		<p class={ active: page == 'security' } onmousedown={ setPage.bind(null, 'security') }>%fa:unlock-alt .fw%%i18n:desktop.tags.mk-settings.security%</p>
@@ -20,6 +20,11 @@
 			<a href="/i/customize-home" class="ui button">ホームをカスタマイズ</a>
 		</section>
 
+		<section class="drive" show={ page == 'drive' }>
+			<h1>%i18n:desktop.tags.mk-settings.drive%</h1>
+			<mk-drive-setting/>
+		</section>
+
 		<section class="apps" show={ page == 'apps' }>
 			<h1>アプリケーション</h1>
 			<mk-authorized-apps/>
@@ -308,3 +313,64 @@
 		};
 	</script>
 </mk-2fa-setting>
+
+<mk-drive-setting>
+	<svg viewBox="0 0 1 1" preserveAspectRatio="none">
+		<circle
+			riot-r={ r }
+			cx="50%" cy="50%"
+			fill="none"
+			stroke-width="0.1"
+			stroke="rgba(0, 0, 0, 0.05)"/>
+		<circle
+			riot-r={ r }
+			cx="50%" cy="50%"
+			riot-stroke-dasharray={ Math.PI * (r * 2) }
+			riot-stroke-dashoffset={ strokeDashoffset }
+			fill="none"
+			stroke-width="0.1"
+			riot-stroke={ color }/>
+		<text x="50%" y="50%" dy="0.05" text-anchor="middle">{ (usageP * 100).toFixed(0) }%</text>
+	</svg>
+
+	<style>
+		:scope
+			display block
+			color #4a535a
+
+			> svg
+				display block
+				height 128px
+
+				> circle
+					transform-origin center
+					transform rotate(-90deg)
+					transition stroke-dashoffset 0.5s ease
+
+				> text
+					font-size 0.15px
+					fill rgba(0, 0, 0, 0.6)
+
+	</style>
+	<script>
+		this.mixin('api');
+
+		this.r = 0.4;
+
+		this.on('mount', () => {
+			this.api('drive').then(info => {
+				const usageP = info.usage / info.capacity * 100;
+				const color = `hsl(${180 - (usageP * 180)}, 80%, 70%)`;
+				const strokeDashoffset = (1 - usageP) * (Math.PI * (this.r * 2));
+
+				this.update({
+					color,
+					strokeDashoffset,
+					usageP,
+					usage: info.usage,
+					capacity: info.capacity
+				});
+			});
+		});
+	</script>
+</mk-drive-setting>

From 39dacbb58fa677dcc881cac6f466312f4a64b464 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 9 Dec 2017 23:03:48 +0900
Subject: [PATCH 030/186] :+1:

---
 src/file/server.ts | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/src/file/server.ts b/src/file/server.ts
index 187ce75c2..990b6ccbe 100644
--- a/src/file/server.ts
+++ b/src/file/server.ts
@@ -135,6 +135,8 @@ async function sendFileById(req: express.Request, res: express.Response): Promis
 	}
 
 	const fileId = new mongodb.ObjectID(req.params.id);
+
+	// Fetch (drive) file
 	const file = await DriveFile.findOne({ _id: fileId });
 
 	// validate name

From d0cfe112e11c52acaeebb42c3229a37af23c846a Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 10 Dec 2017 02:45:32 +0900
Subject: [PATCH 031/186] #973

---
 locales/en.yml                        |  3 +++
 locales/ja.yml                        |  3 +++
 src/api/endpoints.ts                  | 17 ++++++++++++----
 src/api/endpoints/i/2fa/unregister.ts | 28 +++++++++++++++++++++++++++
 src/web/app/desktop/tags/settings.tag | 21 +++++++++++++++++++-
 5 files changed, 67 insertions(+), 5 deletions(-)
 create mode 100644 src/api/endpoints/i/2fa/unregister.ts

diff --git a/locales/en.yml b/locales/en.yml
index 4dadcb806..8392e170c 100644
--- a/locales/en.yml
+++ b/locales/en.yml
@@ -311,6 +311,9 @@ desktop:
       url: "https://www.google.com/landing/2step/"
       caution: "As a caveat, security improves, but you can not sign in to Misskey if you lose a registered device, etc."
       register: "Register a device"
+      already-registered: "The setting has already been completed."
+      unregister: "Disable"
+      unregistered: "Two-step authentication has been disabled."
       enter-password: "Enter the password"
       authenticator: "First, you need install Google Authenticator to your device:"
       howtoinstall: "How to install"
diff --git a/locales/ja.yml b/locales/ja.yml
index d38ae682e..f9d41d909 100644
--- a/locales/ja.yml
+++ b/locales/ja.yml
@@ -311,6 +311,9 @@ desktop:
       url: "https://www.google.co.jp/intl/ja/landing/2step/"
       caution: "登録したデバイスを紛失するなどした場合、Misskeyにサインインできなくなりますのでご注意ください。"
       register: "デバイスを登録する"
+      already-registered: "既に設定は完了しています。"
+      unregister: "設定を解除"
+      unregistered: "二段階認証が無効になりました。"
       enter-password: "パスワードを入力してください"
       authenticator: "まず、Google Authenticatorをお使いのデバイスにインストールします:"
       howtoinstall: "インストール方法はこちら"
diff --git a/src/api/endpoints.ts b/src/api/endpoints.ts
index 49871c0ce..1138df193 100644
--- a/src/api/endpoints.ts
+++ b/src/api/endpoints.ts
@@ -157,11 +157,18 @@ const endpoints: Endpoint[] = [
 	},
 	{
 		name: 'i/2fa/register',
-		withCredential: true
+		withCredential: true,
+		secure: true
+	},
+	{
+		name: 'i/2fa/unregister',
+		withCredential: true,
+		secure: true
 	},
 	{
 		name: 'i/2fa/done',
-		withCredential: true
+		withCredential: true,
+		secure: true
 	},
 	{
 		name: 'i/update',
@@ -179,11 +186,13 @@ const endpoints: Endpoint[] = [
 	},
 	{
 		name: 'i/change_password',
-		withCredential: true
+		withCredential: true,
+		secure: true
 	},
 	{
 		name: 'i/regenerate_token',
-		withCredential: true
+		withCredential: true,
+		secure: true
 	},
 	{
 		name: 'i/pin',
diff --git a/src/api/endpoints/i/2fa/unregister.ts b/src/api/endpoints/i/2fa/unregister.ts
new file mode 100644
index 000000000..6bee6a26f
--- /dev/null
+++ b/src/api/endpoints/i/2fa/unregister.ts
@@ -0,0 +1,28 @@
+/**
+ * Module dependencies
+ */
+import $ from 'cafy';
+import * as bcrypt from 'bcryptjs';
+import User from '../../../models/user';
+
+module.exports = async (params, user) => new Promise(async (res, rej) => {
+	// Get 'password' parameter
+	const [password, passwordErr] = $(params.password).string().$;
+	if (passwordErr) return rej('invalid password param');
+
+	// Compare password
+	const same = await bcrypt.compare(password, user.password);
+
+	if (!same) {
+		return rej('incorrect password');
+	}
+
+	await User.update(user._id, {
+		$set: {
+			two_factor_secret: null,
+			two_factor_enabled: false
+		}
+	});
+
+	res();
+});
diff --git a/src/web/app/desktop/tags/settings.tag b/src/web/app/desktop/tags/settings.tag
index 9f71da687..3fa1f50fc 100644
--- a/src/web/app/desktop/tags/settings.tag
+++ b/src/web/app/desktop/tags/settings.tag
@@ -266,7 +266,11 @@
 <mk-2fa-setting>
 	<p>%i18n:desktop.tags.mk-2fa-setting.intro%<a href="%i18n:desktop.tags.mk-2fa-setting.url%" target="_blank">%i18n:desktop.tags.mk-2fa-setting.detail%</a></p>
 	<div class="ui info warn"><p>%fa:exclamation-triangle%%i18n:desktop.tags.mk-2fa-setting.caution%</p></div>
-	<p if={ !data }><button onclick={ register } class="ui primary">%i18n:desktop.tags.mk-2fa-setting.register%</button></p>
+	<p if={ !data && !I.two_factor_enabled }><button onclick={ register } class="ui primary">%i18n:desktop.tags.mk-2fa-setting.register%</button></p>
+	<virtual if={ I.two_factor_enabled }>
+		<p>%i18n:desktop.tags.mk-2fa-setting.already-registered%</p>
+		<button onclick={ unregister } class="ui">%i18n:desktop.tags.mk-2fa-setting.unregister%</button>
+	</virtual>
 	<div if={ data }>
 		<ol>
 			<li>%i18n:desktop.tags.mk-2fa-setting.authenticator% <a href="https://support.google.com/accounts/answer/1066447" target="_blank">%i18n:desktop.tags.mk-2fa-setting.howtoinstall%</a></li>
@@ -288,6 +292,7 @@
 		import passwordDialog from '../scripts/password-dialog';
 		import notify from '../scripts/notify';
 
+		this.mixin('i');
 		this.mixin('api');
 
 		this.register = () => {
@@ -302,11 +307,25 @@
 			});
 		};
 
+		this.unregister = () => {
+			passwordDialog('%i18n:desktop.tags.mk-2fa-setting.enter-password%', password => {
+				this.api('i/2fa/unregister', {
+					password: password
+				}).then(data => {
+					notify('%i18n:desktop.tags.mk-2fa-setting.unregistered%');
+					this.I.two_factor_enabled = false;
+					this.I.update();
+				});
+			});
+		};
+
 		this.submit = () => {
 			this.api('i/2fa/done', {
 				token: this.refs.token.value
 			}).then(() => {
 				notify('%i18n:desktop.tags.mk-2fa-setting.success%');
+				this.I.two_factor_enabled = true;
+				this.I.update();
 			}).catch(() => {
 				notify('%i18n:desktop.tags.mk-2fa-setting.failed%');
 			});

From c4369a3489c8501e18c6840a294f663cf9e9c820 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 10 Dec 2017 02:48:22 +0900
Subject: [PATCH 032/186] Fix bug

---
 src/web/app/desktop/tags/settings.tag | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/web/app/desktop/tags/settings.tag b/src/web/app/desktop/tags/settings.tag
index 3fa1f50fc..f7ecfe3e8 100644
--- a/src/web/app/desktop/tags/settings.tag
+++ b/src/web/app/desktop/tags/settings.tag
@@ -378,7 +378,7 @@
 
 		this.on('mount', () => {
 			this.api('drive').then(info => {
-				const usageP = info.usage / info.capacity * 100;
+				const usageP = info.usage / info.capacity;
 				const color = `hsl(${180 - (usageP * 180)}, 80%, 70%)`;
 				const strokeDashoffset = (1 - usageP) * (Math.PI * (this.r * 2));
 

From 386d915d8953dc45da013471ac8dbb5f3f7ce9b6 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 10 Dec 2017 02:57:23 +0900
Subject: [PATCH 033/186] :art:

---
 src/web/app/desktop/tags/ui.tag          | 3 +++
 src/web/app/mobile/tags/drive/folder.tag | 3 +++
 2 files changed, 6 insertions(+)

diff --git a/src/web/app/desktop/tags/ui.tag b/src/web/app/desktop/tags/ui.tag
index 052568062..bc0a937a5 100644
--- a/src/web/app/desktop/tags/ui.tag
+++ b/src/web/app/desktop/tags/ui.tag
@@ -146,6 +146,9 @@
 					color #9eaba8
 					pointer-events none
 
+					> *
+						vertical-align middle
+
 				> input
 					user-select text
 					cursor auto
diff --git a/src/web/app/mobile/tags/drive/folder.tag b/src/web/app/mobile/tags/drive/folder.tag
index da55cf474..c1b44dcb2 100644
--- a/src/web/app/mobile/tags/drive/folder.tag
+++ b/src/web/app/mobile/tags/drive/folder.tag
@@ -39,6 +39,9 @@
 						width 1em
 						height 1em
 
+						> *
+							vertical-align initial
+
 	</style>
 	<script>
 		this.browser = this.parent;

From d6cc08b1350db0c873c9f76d2f89651a6a717ba5 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 10 Dec 2017 03:09:30 +0900
Subject: [PATCH 034/186] :art:

---
 .../app/desktop/tags/select-file-from-drive-window.tag | 10 +++++-----
 1 file changed, 5 insertions(+), 5 deletions(-)

diff --git a/src/web/app/desktop/tags/select-file-from-drive-window.tag b/src/web/app/desktop/tags/select-file-from-drive-window.tag
index 17cc60798..c660a2fe9 100644
--- a/src/web/app/desktop/tags/select-file-from-drive-window.tag
+++ b/src/web/app/desktop/tags/select-file-from-drive-window.tag
@@ -33,7 +33,7 @@
 						height 72px
 						background lighten($theme-color, 95%)
 
-						.upload
+						> .upload
 							display inline-block
 							position absolute
 							top 8px
@@ -72,8 +72,8 @@
 									border 2px solid rgba($theme-color, 0.3)
 									border-radius 8px
 
-						.ok
-						.cancel
+						> .ok
+						> .cancel
 							display block
 							position absolute
 							bottom 16px
@@ -102,7 +102,7 @@
 								opacity 0.7
 								cursor default
 
-						.ok
+						> .ok
 							right 16px
 							color $theme-color-foreground
 							background linear-gradient(to bottom, lighten($theme-color, 25%) 0%, lighten($theme-color, 10%) 100%)
@@ -119,7 +119,7 @@
 								background $theme-color
 								border-color $theme-color
 
-						.cancel
+						> .cancel
 							right 148px
 							color #888
 							background linear-gradient(to bottom, #ffffff 0%, #f5f5f5 100%)

From 2a8931b20969fae171100cef2149fa1393e3439a Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 10 Dec 2017 03:10:08 +0900
Subject: [PATCH 035/186] v3320

---
 CHANGELOG.md | 4 ++++
 package.json | 2 +-
 2 files changed, 5 insertions(+), 1 deletion(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6d0550b88..77db8b94f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,10 @@ ChangeLog (Release Notes)
 =========================
 主に notable な changes を書いていきます
 
+3320 (2017/12/10)
+-----------------
+* なんか
+
 3310 (2017/12/09)
 -----------------
 * i18nなど
diff --git a/package.json b/package.json
index c932e1646..014013fa9 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
 	"name": "misskey",
 	"author": "syuilo <i@syuilo.com>",
-	"version": "0.0.3310",
+	"version": "0.0.3320",
 	"license": "MIT",
 	"description": "A miniblog-based SNS",
 	"bugs": "https://github.com/syuilo/misskey/issues",

From c1a676c3ac661a3505c393ba09fc6df7b6743235 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 10 Dec 2017 03:33:28 +0900
Subject: [PATCH 036/186] :art:

---
 src/web/app/mobile/tags/drive/folder.tag | 7 ++-----
 1 file changed, 2 insertions(+), 5 deletions(-)

diff --git a/src/web/app/mobile/tags/drive/folder.tag b/src/web/app/mobile/tags/drive/folder.tag
index c1b44dcb2..6125e0b25 100644
--- a/src/web/app/mobile/tags/drive/folder.tag
+++ b/src/web/app/mobile/tags/drive/folder.tag
@@ -34,13 +34,10 @@
 						position absolute
 						top 0
 						bottom 0
-						right 8px
-						margin auto 0 auto 0
-						width 1em
-						height 1em
+						right 20px
 
 						> *
-							vertical-align initial
+							height 100%
 
 	</style>
 	<script>

From 0c4de8f1a90bc5c0a588e763921ad9051c5d0464 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 10 Dec 2017 03:33:49 +0900
Subject: [PATCH 037/186] v3322

---
 CHANGELOG.md | 4 ++++
 package.json | 2 +-
 2 files changed, 5 insertions(+), 1 deletion(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 77db8b94f..1274b0fe9 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,10 @@ ChangeLog (Release Notes)
 =========================
 主に notable な changes を書いていきます
 
+3322 (2017/12/10)
+-----------------
+* :art:
+
 3320 (2017/12/10)
 -----------------
 * なんか
diff --git a/package.json b/package.json
index 014013fa9..369113ba6 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
 	"name": "misskey",
 	"author": "syuilo <i@syuilo.com>",
-	"version": "0.0.3320",
+	"version": "0.0.3322",
 	"license": "MIT",
 	"description": "A miniblog-based SNS",
 	"bugs": "https://github.com/syuilo/misskey/issues",

From 4cb518400b2f867a86278f7b85687e51b43dadff Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Sun, 10 Dec 2017 04:16:20 +0900
Subject: [PATCH 038/186] Update README.md

---
 README.md | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/README.md b/README.md
index e3c93b749..68a8f4b38 100644
--- a/README.md
+++ b/README.md
@@ -16,7 +16,7 @@ Key features
 ----------------------------------------------------------------
 * Automatically updated timeline
 * Private messages
-* Free 1GB storage for each all users
+* Two-Factor Authentication support
 * ServiceWorker support
 * Web API for third-party applications
 * No ads

From c60b83f0dd6febc7842612acf16fe1bc1d523987 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 10 Dec 2017 17:50:02 +0900
Subject: [PATCH 039/186] Refactor

---
 src/api/server.ts          |  7 -------
 src/api/service/twitter.ts | 34 +++++++++++++++++++++++-----------
 2 files changed, 23 insertions(+), 18 deletions(-)

diff --git a/src/api/server.ts b/src/api/server.ts
index 026357b46..463b3f017 100644
--- a/src/api/server.ts
+++ b/src/api/server.ts
@@ -49,13 +49,6 @@ endpoints.forEach(endpoint =>
 app.post('/signup', require('./private/signup').default);
 app.post('/signin', require('./private/signin').default);
 
-app.use((req, res, next) => {
-	// req.headers['cookie'] は常に string ですが、型定義の都合上
-	// string | string[] になっているので string を明示しています
-	res.locals.user = ((req.headers['cookie'] as string || '').match(/i=(!\w+)/) || [null, null])[1];
-	next();
-});
-
 require('./service/github')(app);
 require('./service/twitter')(app);
 
diff --git a/src/api/service/twitter.ts b/src/api/service/twitter.ts
index f164cdc45..e03cd5acc 100644
--- a/src/api/service/twitter.ts
+++ b/src/api/service/twitter.ts
@@ -12,15 +12,24 @@ import config from '../../conf';
 import signin from '../common/signin';
 
 module.exports = (app: express.Application) => {
+	function getUserToken(req) {
+		// req.headers['cookie'] は常に string ですが、型定義の都合上
+		// string | string[] になっているので string を明示しています
+		return ((req.headers['cookie'] as string || '').match(/i=(!\w+)/) || [null, null])[1];
+	}
+
 	app.get('/disconnect/twitter', async (req, res): Promise<any> => {
-		if (res.locals.user == null) return res.send('plz signin');
+		const userToken = getUserToken(req);
+
+		if (userToken == null) return res.send('plz signin');
+
 		const user = await User.findOneAndUpdate({
-			token: res.locals.user
+			token: userToken
 		}, {
-				$set: {
-					twitter: null
-				}
-			});
+			$set: {
+				twitter: null
+			}
+		});
 
 		res.send(`Twitterの連携を解除しました :v:`);
 
@@ -50,9 +59,10 @@ module.exports = (app: express.Application) => {
 	});
 
 	app.get('/connect/twitter', async (req, res): Promise<any> => {
-		if (res.locals.user == null) return res.send('plz signin');
+		const userToken = getUserToken(req);
+		if (userToken == null) return res.send('plz signin');
 		const ctx = await twAuth.begin();
-		redis.set(res.locals.user, JSON.stringify(ctx));
+		redis.set(userToken, JSON.stringify(ctx));
 		res.redirect(ctx.url);
 	});
 
@@ -77,7 +87,9 @@ module.exports = (app: express.Application) => {
 	});
 
 	app.get('/tw/cb', (req, res): any => {
-		if (res.locals.user == null) {
+		const userToken = getUserToken(req);
+
+		if (userToken == null) {
 			// req.headers['cookie'] は常に string ですが、型定義の都合上
 			// string | string[] になっているので string を明示しています
 			const cookies = cookie.parse((req.headers['cookie'] as string || ''));
@@ -102,11 +114,11 @@ module.exports = (app: express.Application) => {
 				signin(res, user, true);
 			});
 		} else {
-			redis.get(res.locals.user, async (_, ctx) => {
+			redis.get(userToken, async (_, ctx) => {
 				const result = await twAuth.done(JSON.parse(ctx), req.query.oauth_verifier);
 
 				const user = await User.findOneAndUpdate({
-					token: res.locals.user
+					token: userToken
 				}, {
 					$set: {
 						twitter: {

From 6f2fde0304f96089c9d6f05546ec3bbe5224a4b0 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 10 Dec 2017 18:08:28 +0900
Subject: [PATCH 040/186] =?UTF-8?q?=E4=BB=96=E3=81=AE=E3=82=A6=E3=82=A7?=
 =?UTF-8?q?=E3=83=96=E3=82=B5=E3=82=A4=E3=83=88=E3=81=8B=E3=82=89=E7=9B=B4?=
 =?UTF-8?q?=E6=8E=A5MisskeyAPI=E3=82=92=E5=88=A9=E7=94=A8=E3=81=A7?=
 =?UTF-8?q?=E3=81=8D=E3=82=8B=E3=82=88=E3=81=86=E3=81=AB?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 src/api/server.ts          |  4 +---
 src/api/service/twitter.ts | 39 ++++++++++++++++++++++++++++++++++----
 2 files changed, 36 insertions(+), 7 deletions(-)

diff --git a/src/api/server.ts b/src/api/server.ts
index 463b3f017..e89d19609 100644
--- a/src/api/server.ts
+++ b/src/api/server.ts
@@ -26,9 +26,7 @@ app.use(bodyParser.json({
 		}
 	}
 }));
-app.use(cors({
-	origin: true
-}));
+app.use(cors());
 
 app.get('/', (req, res) => {
 	res.send('YEE HAW');
diff --git a/src/api/service/twitter.ts b/src/api/service/twitter.ts
index e03cd5acc..573895e8f 100644
--- a/src/api/service/twitter.ts
+++ b/src/api/service/twitter.ts
@@ -12,15 +12,31 @@ import config from '../../conf';
 import signin from '../common/signin';
 
 module.exports = (app: express.Application) => {
-	function getUserToken(req) {
+	function getUserToken(req: express.Request) {
 		// req.headers['cookie'] は常に string ですが、型定義の都合上
 		// string | string[] になっているので string を明示しています
 		return ((req.headers['cookie'] as string || '').match(/i=(!\w+)/) || [null, null])[1];
 	}
 
-	app.get('/disconnect/twitter', async (req, res): Promise<any> => {
-		const userToken = getUserToken(req);
+	function compareOrigin(req: express.Request) {
+		function normalizeUrl(url: string) {
+			return url[url.length - 1] === '/' ? url.substr(0, url.length - 1) : url;
+		}
 
+		// req.headers['cookie'] は常に string ですが、型定義の都合上
+		// string | string[] になっているので string を明示しています
+		const referer = req.headers['referer'] as string;
+
+		return (normalizeUrl(referer) == normalizeUrl(config.url));
+	}
+
+	app.get('/disconnect/twitter', async (req, res): Promise<any> => {
+		if (!compareOrigin(req)) {
+			res.status(400).send('invalid origin');
+			return;
+		}
+
+		const userToken = getUserToken(req);
 		if (userToken == null) return res.send('plz signin');
 
 		const user = await User.findOneAndUpdate({
@@ -59,8 +75,14 @@ module.exports = (app: express.Application) => {
 	});
 
 	app.get('/connect/twitter', async (req, res): Promise<any> => {
+		if (!compareOrigin(req)) {
+			res.status(400).send('invalid origin');
+			return;
+		}
+
 		const userToken = getUserToken(req);
 		if (userToken == null) return res.send('plz signin');
+
 		const ctx = await twAuth.begin();
 		redis.set(userToken, JSON.stringify(ctx));
 		res.redirect(ctx.url);
@@ -98,6 +120,7 @@ module.exports = (app: express.Application) => {
 
 			if (sessid == undefined) {
 				res.status(400).send('invalid session');
+				return;
 			}
 
 			redis.get(sessid, async (_, ctx) => {
@@ -109,13 +132,21 @@ module.exports = (app: express.Application) => {
 
 				if (user == null) {
 					res.status(404).send(`@${result.screenName}と連携しているMisskeyアカウントはありませんでした...`);
+					return;
 				}
 
 				signin(res, user, true);
 			});
 		} else {
+			const verifier = req.query.oauth_verifier;
+
+			if (verifier == null) {
+				res.status(400).send('invalid session');
+				return;
+			}
+
 			redis.get(userToken, async (_, ctx) => {
-				const result = await twAuth.done(JSON.parse(ctx), req.query.oauth_verifier);
+				const result = await twAuth.done(JSON.parse(ctx), verifier);
 
 				const user = await User.findOneAndUpdate({
 					token: userToken

From eb0975e8393e6723f7674203c2a18f3c0589109a Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 10 Dec 2017 18:12:39 +0900
Subject: [PATCH 041/186] oops

---
 src/api/service/twitter.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/api/service/twitter.ts b/src/api/service/twitter.ts
index 573895e8f..0e75ee0bd 100644
--- a/src/api/service/twitter.ts
+++ b/src/api/service/twitter.ts
@@ -23,7 +23,7 @@ module.exports = (app: express.Application) => {
 			return url[url.length - 1] === '/' ? url.substr(0, url.length - 1) : url;
 		}
 
-		// req.headers['cookie'] は常に string ですが、型定義の都合上
+		// req.headers['referer'] は常に string ですが、型定義の都合上
 		// string | string[] になっているので string を明示しています
 		const referer = req.headers['referer'] as string;
 

From e51bd95a8d5e7bd006ab2f3db67b9c3cc2cab7cf Mon Sep 17 00:00:00 2001
From: tamaina <tamaina@hotmail.co.jp>
Date: Sun, 10 Dec 2017 20:39:26 +0900
Subject: [PATCH 042/186] =?UTF-8?q?channel=E4=BB=A5=E5=A4=96mk-images-view?=
 =?UTF-8?q?er=E5=8C=96=E3=83=BBmk-images-viewer=E5=BC=B7=E5=8C=96?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 src/web/app/desktop/tags/images-viewer.tag   | 49 ++++++++++++++------
 src/web/app/desktop/tags/post-detail-sub.tag |  6 +--
 src/web/app/desktop/tags/post-detail.tag     |  7 +--
 3 files changed, 38 insertions(+), 24 deletions(-)

diff --git a/src/web/app/desktop/tags/images-viewer.tag b/src/web/app/desktop/tags/images-viewer.tag
index 44a61cb74..3edd1300b 100644
--- a/src/web/app/desktop/tags/images-viewer.tag
+++ b/src/web/app/desktop/tags/images-viewer.tag
@@ -1,32 +1,46 @@
 <mk-images-viewer>
-	<div class="image" ref="view" onmousemove={ mousemove } style={ 'background-image: url(' + image.url + '?thumbnail' } onclick={ click }><img src={ image.url + '?thumbnail&size=512' } alt={ image.name } title={ image.name }/></div>
+	<virtual each={ image in images }>
+		<mk-images-viewer-image ref="wrap" image={ image } images={ images }/>
+	</virtual>
 	<style>
 		:scope
-			display block
+			display grid
 			overflow hidden
 			border-radius 4px
+			grid-gap .25em
 
-			> .image
+			> div
 				cursor zoom-in
+				overflow hidden
+				background-position center
 
 				> img
-					display block
-					max-height 256px
-					max-width 100%
-					margin 0 auto
-
-				&:hover
-					> img
-						visibility hidden
+					visibility hidden
+					max-width: 100%
+					max-height: 256px
 
 				&:not(:hover)
-					background-image none !important
+					background-size cover
 
+				&:nth-child(1):nth-last-child(3)
+					grid-row 1 / 3
 	</style>
 	<script>
 		this.images = this.opts.images;
-		this.image = this.images[0];
 
+		this.on('mount', () => {
+			if(this.images.length >= 3) this.refs.wrap.style.gridAutoRows = "9em";
+			if(this.images.length == 2) this.refs.wrap.style.gridAutoRows = "12em";
+			if(this.images.length == 1) this.refs.wrap.style.gridAutoRows = "256px";
+			if(this.images.length == 4 || this.images.length == 2) this.refs.wrap.style.gridTemplateColumns = "repeat(2, 1fr)";
+			if(this.images.length == 3) this.refs.wrap.style.gridTemplateColumns = "65% 1fr";
+		})
+	</script>
+</mk-images-viewer>
+
+<mk-images-viewer-image>
+	<div ref="view" onmousemove={ mousemove } onmouseleave={ mouseleave } style={ 'background-image: url(' + image.url + '?thumbnail?size=512' } onclick={ click }><img ref="image" src={ image.url + '?thumbnail&size=512' } alt={ image.name } title={ image.name }/></div>
+	<script>
 		this.mousemove = e => {
 			const rect = this.refs.view.getBoundingClientRect();
 			const mouseX = e.clientX - rect.left;
@@ -34,12 +48,19 @@
 			const xp = mouseX / this.refs.view.offsetWidth * 100;
 			const yp = mouseY / this.refs.view.offsetHeight * 100;
 			this.refs.view.style.backgroundPosition = xp + '% ' + yp + '%';
+			this.refs.view.style.backgroundImage = 'url("' + this.image.url + '?thumbnail")';
 		};
 
+		this.mouseleave = () => {
+			this.refs.view.style.backgroundPosition = "";
+		}
+
 		this.click = () => {
 			riot.mount(document.body.appendChild(document.createElement('mk-image-dialog')), {
 				image: this.image
 			});
 		};
+
+		this.image = this.opts.image;
 	</script>
-</mk-images-viewer>
+</mk-images-viewer-image>
\ No newline at end of file
diff --git a/src/web/app/desktop/tags/post-detail-sub.tag b/src/web/app/desktop/tags/post-detail-sub.tag
index e22386df9..99899929d 100644
--- a/src/web/app/desktop/tags/post-detail-sub.tag
+++ b/src/web/app/desktop/tags/post-detail-sub.tag
@@ -9,7 +9,7 @@
 				<span class="username">@{ post.user.username }</span>
 			</div>
 			<div class="right">
-				<a class="time" href={ '/' + this.post.user.username + '/' + this.post.id }>
+				<a class="time" href={ '/' + post.user.username + '/' + post.id }>
 					<mk-time time={ post.created_at }/>
 				</a>
 			</div>
@@ -17,9 +17,7 @@
 		<div class="body">
 			<div class="text" ref="text"></div>
 			<div class="media" if={ post.media }>
-				<virtual each={ file in post.media }>
-					<img src={ file.url + '?thumbnail&size=512' } alt={ file.name } title={ file.name }/>
-				</virtual>
+				<mk-images-viewer images={ post.media }/>
 			</div>
 		</div>
 	</div>
diff --git a/src/web/app/desktop/tags/post-detail.tag b/src/web/app/desktop/tags/post-detail.tag
index 37f90a6ff..23f7a4198 100644
--- a/src/web/app/desktop/tags/post-detail.tag
+++ b/src/web/app/desktop/tags/post-detail.tag
@@ -37,7 +37,7 @@
 			<div class="body">
 				<div class="text" ref="text"></div>
 				<div class="media" if={ p.media }>
-					<virtual each={ file in p.media }><img src={ file.url + '?thumbnail&size=512' } alt={ file.name } title={ file.name }/></virtual>
+					<mk-images-viewer images={ p.media }/>
 				</div>
 				<mk-poll if={ p.poll } post={ p }/>
 			</div>
@@ -208,11 +208,6 @@
 							> mk-url-preview
 								margin-top 8px
 
-						> .media
-							> img
-								display block
-								max-width 100%
-
 					> footer
 						font-size 1.2em
 

From 4da6fafe43b61c191e193b6bef985ae2c30e0f3c Mon Sep 17 00:00:00 2001
From: tamaina <tamaina@hotmail.co.jp>
Date: Sun, 10 Dec 2017 20:49:12 +0900
Subject: [PATCH 043/186] =?UTF-8?q?URL=E4=BF=AE=E6=AD=A3?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 src/web/app/desktop/tags/images-viewer.tag | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/web/app/desktop/tags/images-viewer.tag b/src/web/app/desktop/tags/images-viewer.tag
index 3edd1300b..9fd6b8de9 100644
--- a/src/web/app/desktop/tags/images-viewer.tag
+++ b/src/web/app/desktop/tags/images-viewer.tag
@@ -39,7 +39,7 @@
 </mk-images-viewer>
 
 <mk-images-viewer-image>
-	<div ref="view" onmousemove={ mousemove } onmouseleave={ mouseleave } style={ 'background-image: url(' + image.url + '?thumbnail?size=512' } onclick={ click }><img ref="image" src={ image.url + '?thumbnail&size=512' } alt={ image.name } title={ image.name }/></div>
+	<div ref="view" onmousemove={ mousemove } onmouseleave={ mouseleave } style={ 'background-image: url(' + image.url + '?thumbnail&size=512' } onclick={ click }><img ref="image" src={ image.url + '?thumbnail&size=512' } alt={ image.name } title={ image.name }/></div>
 	<script>
 		this.mousemove = e => {
 			const rect = this.refs.view.getBoundingClientRect();

From 3cd0c45176b85870c345c32e5ba19f71a63309c2 Mon Sep 17 00:00:00 2001
From: tamaina <tamaina@hotmail.co.jp>
Date: Sun, 10 Dec 2017 21:01:25 +0900
Subject: [PATCH 044/186] =?UTF-8?q?=E6=8E=A8=E6=B8=AC=E3=81=A7CSS=E3=82=92?=
 =?UTF-8?q?=E5=8B=95=E3=81=8B=E3=81=99?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 src/web/app/desktop/tags/images-viewer.tag   | 1 +
 src/web/app/desktop/tags/post-detail-sub.tag | 5 -----
 src/web/app/desktop/tags/timeline.tag        | 5 -----
 3 files changed, 1 insertion(+), 10 deletions(-)

diff --git a/src/web/app/desktop/tags/images-viewer.tag b/src/web/app/desktop/tags/images-viewer.tag
index 9fd6b8de9..0369ea9f6 100644
--- a/src/web/app/desktop/tags/images-viewer.tag
+++ b/src/web/app/desktop/tags/images-viewer.tag
@@ -15,6 +15,7 @@
 				background-position center
 
 				> img
+					display block
 					visibility hidden
 					max-width: 100%
 					max-height: 256px
diff --git a/src/web/app/desktop/tags/post-detail-sub.tag b/src/web/app/desktop/tags/post-detail-sub.tag
index 99899929d..ab45b5523 100644
--- a/src/web/app/desktop/tags/post-detail-sub.tag
+++ b/src/web/app/desktop/tags/post-detail-sub.tag
@@ -105,11 +105,6 @@
 						> mk-url-preview
 							margin-top 8px
 
-					> .media
-						> img
-							display block
-							max-width 100%
-
 	</style>
 	<script>
 		import compile from '../../common/scripts/text-compiler';
diff --git a/src/web/app/desktop/tags/timeline.tag b/src/web/app/desktop/tags/timeline.tag
index 08e658a3c..77e4a573b 100644
--- a/src/web/app/desktop/tags/timeline.tag
+++ b/src/web/app/desktop/tags/timeline.tag
@@ -357,11 +357,6 @@
 								background $theme-color
 								border-radius 4px
 
-						> .media
-							> img
-								display block
-								max-width 100%
-
 						> mk-poll
 							font-size 80%
 

From 6bf5a4edffc8eac26d10f0880317618e1861cc71 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 10 Dec 2017 21:57:17 +0900
Subject: [PATCH 045/186] :v:

---
 src/web/app/desktop/tags/images-viewer.tag | 79 ++++++++++++++++------
 1 file changed, 58 insertions(+), 21 deletions(-)

diff --git a/src/web/app/desktop/tags/images-viewer.tag b/src/web/app/desktop/tags/images-viewer.tag
index 0369ea9f6..1ad382dda 100644
--- a/src/web/app/desktop/tags/images-viewer.tag
+++ b/src/web/app/desktop/tags/images-viewer.tag
@@ -1,17 +1,71 @@
 <mk-images-viewer>
 	<virtual each={ image in images }>
-		<mk-images-viewer-image ref="wrap" image={ image } images={ images }/>
+		<mk-images-viewer-image image={ image } images={ images }/>
 	</virtual>
 	<style>
 		:scope
 			display grid
+			grid-gap .25em
+	</style>
+	<script>
+		this.images = this.opts.images;
+
+		this.on('mount', () => {
+			if (this.images.length == 1) {
+				this.root.style.gridTemplateRows = '256px';
+
+				this.tags['mk-images-viewer-image'].root.style.gridColumn = '1 / 2';
+				this.tags['mk-images-viewer-image'].root.style.gridRow = '1 / 2';
+			} else if (this.images.length == 2) {
+				this.root.style.gridTemplateColumns = '50% 50%';
+				this.root.style.gridTemplateRows = '256px';
+
+				this.tags['mk-images-viewer-image'][0].root.style.gridColumn = '1 / 2';
+				this.tags['mk-images-viewer-image'][0].root.style.gridRow = '1 / 2';
+				this.tags['mk-images-viewer-image'][1].root.style.gridColumn = '2 / 3';
+				this.tags['mk-images-viewer-image'][1].root.style.gridRow = '1 / 2';
+			} else if (this.images.length == 3) {
+				this.root.style.gridTemplateColumns = '70% 30%';
+				this.root.style.gridTemplateRows = '128px 128px';
+
+				this.tags['mk-images-viewer-image'][0].root.style.gridColumn = '1 / 2';
+				this.tags['mk-images-viewer-image'][0].root.style.gridRow = '1 / 3';
+				this.tags['mk-images-viewer-image'][1].root.style.gridColumn = '2 / 3';
+				this.tags['mk-images-viewer-image'][1].root.style.gridRow = '1 / 2';
+				this.tags['mk-images-viewer-image'][2].root.style.gridColumn = '2 / 3';
+				this.tags['mk-images-viewer-image'][2].root.style.gridRow = '2 / 3';
+			} else if (this.images.length == 4) {
+				this.root.style.gridTemplateColumns = '50% 50%';
+				this.root.style.gridTemplateRows = '128px 128px';
+
+				this.tags['mk-images-viewer-image'][0].root.style.gridColumn = '1 / 2';
+				this.tags['mk-images-viewer-image'][0].root.style.gridRow = '1 / 2';
+				this.tags['mk-images-viewer-image'][1].root.style.gridColumn = '2 / 3';
+				this.tags['mk-images-viewer-image'][1].root.style.gridRow = '1 / 2';
+				this.tags['mk-images-viewer-image'][2].root.style.gridColumn = '1 / 2';
+				this.tags['mk-images-viewer-image'][2].root.style.gridRow = '2 / 3';
+				this.tags['mk-images-viewer-image'][3].root.style.gridColumn = '2 / 3';
+				this.tags['mk-images-viewer-image'][3].root.style.gridRow = '2 / 3';
+			}
+		});
+	</script>
+</mk-images-viewer>
+
+<mk-images-viewer-image>
+	<div ref="view" onmousemove={ mousemove } onmouseleave={ mouseleave } style={ 'background-image: url(' + image.url + '?thumbnail&size=512' } onclick={ click }>
+		<img ref="image" src={ image.url + '?thumbnail&size=512' } alt={ image.name } title={ image.name }/>
+	</div>
+	<style>
+		:scope
+			display block
 			overflow hidden
 			border-radius 4px
-			grid-gap .25em
 
 			> div
 				cursor zoom-in
 				overflow hidden
+				width 100%
+				height 100%
 				background-position center
 
 				> img
@@ -23,24 +77,7 @@
 				&:not(:hover)
 					background-size cover
 
-				&:nth-child(1):nth-last-child(3)
-					grid-row 1 / 3
 	</style>
-	<script>
-		this.images = this.opts.images;
-
-		this.on('mount', () => {
-			if(this.images.length >= 3) this.refs.wrap.style.gridAutoRows = "9em";
-			if(this.images.length == 2) this.refs.wrap.style.gridAutoRows = "12em";
-			if(this.images.length == 1) this.refs.wrap.style.gridAutoRows = "256px";
-			if(this.images.length == 4 || this.images.length == 2) this.refs.wrap.style.gridTemplateColumns = "repeat(2, 1fr)";
-			if(this.images.length == 3) this.refs.wrap.style.gridTemplateColumns = "65% 1fr";
-		})
-	</script>
-</mk-images-viewer>
-
-<mk-images-viewer-image>
-	<div ref="view" onmousemove={ mousemove } onmouseleave={ mouseleave } style={ 'background-image: url(' + image.url + '?thumbnail&size=512' } onclick={ click }><img ref="image" src={ image.url + '?thumbnail&size=512' } alt={ image.name } title={ image.name }/></div>
 	<script>
 		this.mousemove = e => {
 			const rect = this.refs.view.getBoundingClientRect();
@@ -54,7 +91,7 @@
 
 		this.mouseleave = () => {
 			this.refs.view.style.backgroundPosition = "";
-		}
+		};
 
 		this.click = () => {
 			riot.mount(document.body.appendChild(document.createElement('mk-image-dialog')), {
@@ -64,4 +101,4 @@
 
 		this.image = this.opts.image;
 	</script>
-</mk-images-viewer-image>
\ No newline at end of file
+</mk-images-viewer-image>

From 1a015457fa251c1f873c8eb81400047d28500c18 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 10 Dec 2017 22:32:09 +0900
Subject: [PATCH 046/186] :art:

---
 src/web/app/desktop/tags/images-viewer.tag    | 104 ------------------
 src/web/app/desktop/tags/images.tag           | 100 +++++++++++++++++
 src/web/app/desktop/tags/index.ts             |   2 +-
 src/web/app/desktop/tags/post-detail-sub.tag  |   2 +-
 src/web/app/desktop/tags/post-detail.tag      |   2 +-
 src/web/app/desktop/tags/sub-post-content.tag |   2 +-
 src/web/app/desktop/tags/timeline.tag         |   2 +-
 src/web/app/mobile/tags/images-viewer.tag     |  26 -----
 src/web/app/mobile/tags/images.tag            |  78 +++++++++++++
 src/web/app/mobile/tags/index.ts              |   2 +-
 src/web/app/mobile/tags/post-detail.tag       |   2 +-
 src/web/app/mobile/tags/sub-post-content.tag  |   2 +-
 src/web/app/mobile/tags/timeline.tag          |   2 +-
 13 files changed, 187 insertions(+), 139 deletions(-)
 delete mode 100644 src/web/app/desktop/tags/images-viewer.tag
 create mode 100644 src/web/app/desktop/tags/images.tag
 delete mode 100644 src/web/app/mobile/tags/images-viewer.tag
 create mode 100644 src/web/app/mobile/tags/images.tag

diff --git a/src/web/app/desktop/tags/images-viewer.tag b/src/web/app/desktop/tags/images-viewer.tag
deleted file mode 100644
index 1ad382dda..000000000
--- a/src/web/app/desktop/tags/images-viewer.tag
+++ /dev/null
@@ -1,104 +0,0 @@
-<mk-images-viewer>
-	<virtual each={ image in images }>
-		<mk-images-viewer-image image={ image } images={ images }/>
-	</virtual>
-	<style>
-		:scope
-			display grid
-			grid-gap .25em
-	</style>
-	<script>
-		this.images = this.opts.images;
-
-		this.on('mount', () => {
-			if (this.images.length == 1) {
-				this.root.style.gridTemplateRows = '256px';
-
-				this.tags['mk-images-viewer-image'].root.style.gridColumn = '1 / 2';
-				this.tags['mk-images-viewer-image'].root.style.gridRow = '1 / 2';
-			} else if (this.images.length == 2) {
-				this.root.style.gridTemplateColumns = '50% 50%';
-				this.root.style.gridTemplateRows = '256px';
-
-				this.tags['mk-images-viewer-image'][0].root.style.gridColumn = '1 / 2';
-				this.tags['mk-images-viewer-image'][0].root.style.gridRow = '1 / 2';
-				this.tags['mk-images-viewer-image'][1].root.style.gridColumn = '2 / 3';
-				this.tags['mk-images-viewer-image'][1].root.style.gridRow = '1 / 2';
-			} else if (this.images.length == 3) {
-				this.root.style.gridTemplateColumns = '70% 30%';
-				this.root.style.gridTemplateRows = '128px 128px';
-
-				this.tags['mk-images-viewer-image'][0].root.style.gridColumn = '1 / 2';
-				this.tags['mk-images-viewer-image'][0].root.style.gridRow = '1 / 3';
-				this.tags['mk-images-viewer-image'][1].root.style.gridColumn = '2 / 3';
-				this.tags['mk-images-viewer-image'][1].root.style.gridRow = '1 / 2';
-				this.tags['mk-images-viewer-image'][2].root.style.gridColumn = '2 / 3';
-				this.tags['mk-images-viewer-image'][2].root.style.gridRow = '2 / 3';
-			} else if (this.images.length == 4) {
-				this.root.style.gridTemplateColumns = '50% 50%';
-				this.root.style.gridTemplateRows = '128px 128px';
-
-				this.tags['mk-images-viewer-image'][0].root.style.gridColumn = '1 / 2';
-				this.tags['mk-images-viewer-image'][0].root.style.gridRow = '1 / 2';
-				this.tags['mk-images-viewer-image'][1].root.style.gridColumn = '2 / 3';
-				this.tags['mk-images-viewer-image'][1].root.style.gridRow = '1 / 2';
-				this.tags['mk-images-viewer-image'][2].root.style.gridColumn = '1 / 2';
-				this.tags['mk-images-viewer-image'][2].root.style.gridRow = '2 / 3';
-				this.tags['mk-images-viewer-image'][3].root.style.gridColumn = '2 / 3';
-				this.tags['mk-images-viewer-image'][3].root.style.gridRow = '2 / 3';
-			}
-		});
-	</script>
-</mk-images-viewer>
-
-<mk-images-viewer-image>
-	<div ref="view" onmousemove={ mousemove } onmouseleave={ mouseleave } style={ 'background-image: url(' + image.url + '?thumbnail&size=512' } onclick={ click }>
-		<img ref="image" src={ image.url + '?thumbnail&size=512' } alt={ image.name } title={ image.name }/>
-	</div>
-	<style>
-		:scope
-			display block
-			overflow hidden
-			border-radius 4px
-
-			> div
-				cursor zoom-in
-				overflow hidden
-				width 100%
-				height 100%
-				background-position center
-
-				> img
-					display block
-					visibility hidden
-					max-width: 100%
-					max-height: 256px
-
-				&:not(:hover)
-					background-size cover
-
-	</style>
-	<script>
-		this.mousemove = e => {
-			const rect = this.refs.view.getBoundingClientRect();
-			const mouseX = e.clientX - rect.left;
-			const mouseY = e.clientY - rect.top;
-			const xp = mouseX / this.refs.view.offsetWidth * 100;
-			const yp = mouseY / this.refs.view.offsetHeight * 100;
-			this.refs.view.style.backgroundPosition = xp + '% ' + yp + '%';
-			this.refs.view.style.backgroundImage = 'url("' + this.image.url + '?thumbnail")';
-		};
-
-		this.mouseleave = () => {
-			this.refs.view.style.backgroundPosition = "";
-		};
-
-		this.click = () => {
-			riot.mount(document.body.appendChild(document.createElement('mk-image-dialog')), {
-				image: this.image
-			});
-		};
-
-		this.image = this.opts.image;
-	</script>
-</mk-images-viewer-image>
diff --git a/src/web/app/desktop/tags/images.tag b/src/web/app/desktop/tags/images.tag
new file mode 100644
index 000000000..ce67d26a9
--- /dev/null
+++ b/src/web/app/desktop/tags/images.tag
@@ -0,0 +1,100 @@
+<mk-images>
+	<virtual each={ image in images }>
+		<mk-images-image image={ image }/>
+	</virtual>
+	<style>
+		:scope
+			display grid
+			grid-gap 4px
+			height 256px
+	</style>
+	<script>
+		this.images = this.opts.images;
+
+		this.on('mount', () => {
+			if (this.images.length == 1) {
+				this.root.style.gridTemplateRows = '1fr';
+
+				this.tags['mk-images-image'].root.style.gridColumn = '1 / 2';
+				this.tags['mk-images-image'].root.style.gridRow = '1 / 2';
+			} else if (this.images.length == 2) {
+				this.root.style.gridTemplateColumns = '1fr 1fr';
+				this.root.style.gridTemplateRows = '1fr';
+
+				this.tags['mk-images-image'][0].root.style.gridColumn = '1 / 2';
+				this.tags['mk-images-image'][0].root.style.gridRow = '1 / 2';
+				this.tags['mk-images-image'][1].root.style.gridColumn = '2 / 3';
+				this.tags['mk-images-image'][1].root.style.gridRow = '1 / 2';
+			} else if (this.images.length == 3) {
+				this.root.style.gridTemplateColumns = '1fr 0.5fr';
+				this.root.style.gridTemplateRows = '1fr 1fr';
+
+				this.tags['mk-images-image'][0].root.style.gridColumn = '1 / 2';
+				this.tags['mk-images-image'][0].root.style.gridRow = '1 / 3';
+				this.tags['mk-images-image'][1].root.style.gridColumn = '2 / 3';
+				this.tags['mk-images-image'][1].root.style.gridRow = '1 / 2';
+				this.tags['mk-images-image'][2].root.style.gridColumn = '2 / 3';
+				this.tags['mk-images-image'][2].root.style.gridRow = '2 / 3';
+			} else if (this.images.length == 4) {
+				this.root.style.gridTemplateColumns = '1fr 1fr';
+				this.root.style.gridTemplateRows = '1fr 1fr';
+
+				this.tags['mk-images-image'][0].root.style.gridColumn = '1 / 2';
+				this.tags['mk-images-image'][0].root.style.gridRow = '1 / 2';
+				this.tags['mk-images-image'][1].root.style.gridColumn = '2 / 3';
+				this.tags['mk-images-image'][1].root.style.gridRow = '1 / 2';
+				this.tags['mk-images-image'][2].root.style.gridColumn = '1 / 2';
+				this.tags['mk-images-image'][2].root.style.gridRow = '2 / 3';
+				this.tags['mk-images-image'][3].root.style.gridColumn = '2 / 3';
+				this.tags['mk-images-image'][3].root.style.gridRow = '2 / 3';
+			}
+		});
+	</script>
+</mk-images>
+
+<mk-images-image>
+	<a ref="view" href={ image.url } onmousemove={ mousemove } onmouseleave={ mouseleave } style={ 'background-image: url(' + image.url + '?thumbnail&size=512' } onclick={ click } title={ image.name }></a>
+	<style>
+		:scope
+			display block
+			overflow hidden
+			border-radius 4px
+
+			> a
+				display block
+				cursor zoom-in
+				overflow hidden
+				width 100%
+				height 100%
+				background-position center
+
+				&:not(:hover)
+					background-size cover
+
+	</style>
+	<script>
+		this.image = this.opts.image;
+
+		this.mousemove = e => {
+			const rect = this.refs.view.getBoundingClientRect();
+			const mouseX = e.clientX - rect.left;
+			const mouseY = e.clientY - rect.top;
+			const xp = mouseX / this.refs.view.offsetWidth * 100;
+			const yp = mouseY / this.refs.view.offsetHeight * 100;
+			this.refs.view.style.backgroundPosition = xp + '% ' + yp + '%';
+			this.refs.view.style.backgroundImage = 'url("' + this.image.url + '?thumbnail")';
+		};
+
+		this.mouseleave = () => {
+			this.refs.view.style.backgroundPosition = '';
+		};
+
+		this.click = ev => {
+			ev.preventDefault();
+			riot.mount(document.body.appendChild(document.createElement('mk-image-dialog')), {
+				image: this.image
+			});
+			return false;
+		};
+	</script>
+</mk-images-image>
diff --git a/src/web/app/desktop/tags/index.ts b/src/web/app/desktop/tags/index.ts
index 3ec1d108a..30a13b584 100644
--- a/src/web/app/desktop/tags/index.ts
+++ b/src/web/app/desktop/tags/index.ts
@@ -76,7 +76,7 @@ require('./set-avatar-suggestion.tag');
 require('./set-banner-suggestion.tag');
 require('./repost-form.tag');
 require('./sub-post-content.tag');
-require('./images-viewer.tag');
+require('./images.tag');
 require('./image-dialog.tag');
 require('./donation.tag');
 require('./users-list.tag');
diff --git a/src/web/app/desktop/tags/post-detail-sub.tag b/src/web/app/desktop/tags/post-detail-sub.tag
index ab45b5523..cccd85c47 100644
--- a/src/web/app/desktop/tags/post-detail-sub.tag
+++ b/src/web/app/desktop/tags/post-detail-sub.tag
@@ -17,7 +17,7 @@
 		<div class="body">
 			<div class="text" ref="text"></div>
 			<div class="media" if={ post.media }>
-				<mk-images-viewer images={ post.media }/>
+				<mk-images images={ post.media }/>
 			</div>
 		</div>
 	</div>
diff --git a/src/web/app/desktop/tags/post-detail.tag b/src/web/app/desktop/tags/post-detail.tag
index 23f7a4198..47c71a6c1 100644
--- a/src/web/app/desktop/tags/post-detail.tag
+++ b/src/web/app/desktop/tags/post-detail.tag
@@ -37,7 +37,7 @@
 			<div class="body">
 				<div class="text" ref="text"></div>
 				<div class="media" if={ p.media }>
-					<mk-images-viewer images={ p.media }/>
+					<mk-images images={ p.media }/>
 				</div>
 				<mk-poll if={ p.poll } post={ p }/>
 			</div>
diff --git a/src/web/app/desktop/tags/sub-post-content.tag b/src/web/app/desktop/tags/sub-post-content.tag
index 8989ff1c5..1a81b545b 100644
--- a/src/web/app/desktop/tags/sub-post-content.tag
+++ b/src/web/app/desktop/tags/sub-post-content.tag
@@ -8,7 +8,7 @@
 	</div>
 	<details if={ post.media }>
 		<summary>({ post.media.length }つのメディア)</summary>
-		<mk-images-viewer images={ post.media }/>
+		<mk-images images={ post.media }/>
 	</details>
 	<details if={ post.poll }>
 		<summary>投票</summary>
diff --git a/src/web/app/desktop/tags/timeline.tag b/src/web/app/desktop/tags/timeline.tag
index 77e4a573b..ed77a9e60 100644
--- a/src/web/app/desktop/tags/timeline.tag
+++ b/src/web/app/desktop/tags/timeline.tag
@@ -120,7 +120,7 @@
 					<a class="quote" if={ p.repost != null }>RP:</a>
 				</div>
 				<div class="media" if={ p.media }>
-					<mk-images-viewer images={ p.media }/>
+					<mk-images images={ p.media }/>
 				</div>
 				<mk-poll if={ p.poll } post={ p } ref="pollViewer"/>
 				<div class="repost" if={ p.repost }>%fa:quote-right -flip-h%
diff --git a/src/web/app/mobile/tags/images-viewer.tag b/src/web/app/mobile/tags/images-viewer.tag
deleted file mode 100644
index 8ef4a50be..000000000
--- a/src/web/app/mobile/tags/images-viewer.tag
+++ /dev/null
@@ -1,26 +0,0 @@
-<mk-images-viewer>
-	<div class="image" ref="view" onclick={ click }><img ref="img" src={ image.url + '?thumbnail&size=512' } alt={ image.name } title={ image.name }/></div>
-	<style>
-		:scope
-			display block
-			overflow hidden
-			border-radius 4px
-
-			> .image
-
-				> img
-					display block
-					max-height 256px
-					max-width 100%
-					margin 0 auto
-
-	</style>
-	<script>
-		this.images = this.opts.images;
-		this.image = this.images[0];
-
-		this.click = () => {
-			window.open(this.image.url);
-		};
-	</script>
-</mk-images-viewer>
diff --git a/src/web/app/mobile/tags/images.tag b/src/web/app/mobile/tags/images.tag
new file mode 100644
index 000000000..aaa80e4fd
--- /dev/null
+++ b/src/web/app/mobile/tags/images.tag
@@ -0,0 +1,78 @@
+<mk-images>
+	<virtual each={ image in images }>
+		<mk-images-image image={ image }/>
+	</virtual>
+	<style>
+		:scope
+			display grid
+			grid-gap 4px
+			height 256px
+
+			@media (max-width 500px)
+				height 192px
+	</style>
+	<script>
+		this.images = this.opts.images;
+
+		this.on('mount', () => {
+			if (this.images.length == 1) {
+				this.root.style.gridTemplateRows = '1fr';
+
+				this.tags['mk-images-image'].root.style.gridColumn = '1 / 2';
+				this.tags['mk-images-image'].root.style.gridRow = '1 / 2';
+			} else if (this.images.length == 2) {
+				this.root.style.gridTemplateColumns = '1fr 1fr';
+				this.root.style.gridTemplateRows = '1fr';
+
+				this.tags['mk-images-image'][0].root.style.gridColumn = '1 / 2';
+				this.tags['mk-images-image'][0].root.style.gridRow = '1 / 2';
+				this.tags['mk-images-image'][1].root.style.gridColumn = '2 / 3';
+				this.tags['mk-images-image'][1].root.style.gridRow = '1 / 2';
+			} else if (this.images.length == 3) {
+				this.root.style.gridTemplateColumns = '1fr 0.5fr';
+				this.root.style.gridTemplateRows = '1fr 1fr';
+
+				this.tags['mk-images-image'][0].root.style.gridColumn = '1 / 2';
+				this.tags['mk-images-image'][0].root.style.gridRow = '1 / 3';
+				this.tags['mk-images-image'][1].root.style.gridColumn = '2 / 3';
+				this.tags['mk-images-image'][1].root.style.gridRow = '1 / 2';
+				this.tags['mk-images-image'][2].root.style.gridColumn = '2 / 3';
+				this.tags['mk-images-image'][2].root.style.gridRow = '2 / 3';
+			} else if (this.images.length == 4) {
+				this.root.style.gridTemplateColumns = '1fr 1fr';
+				this.root.style.gridTemplateRows = '1fr 1fr';
+
+				this.tags['mk-images-image'][0].root.style.gridColumn = '1 / 2';
+				this.tags['mk-images-image'][0].root.style.gridRow = '1 / 2';
+				this.tags['mk-images-image'][1].root.style.gridColumn = '2 / 3';
+				this.tags['mk-images-image'][1].root.style.gridRow = '1 / 2';
+				this.tags['mk-images-image'][2].root.style.gridColumn = '1 / 2';
+				this.tags['mk-images-image'][2].root.style.gridRow = '2 / 3';
+				this.tags['mk-images-image'][3].root.style.gridColumn = '2 / 3';
+				this.tags['mk-images-image'][3].root.style.gridRow = '2 / 3';
+			}
+		});
+	</script>
+</mk-images>
+
+<mk-images-image>
+	<a ref="view" href={ image.url } target="_blank" style={ 'background-image: url(' + image.url + '?thumbnail&size=512' } title={ image.name }></a>
+	<style>
+		:scope
+			display block
+			overflow hidden
+			border-radius 4px
+
+			> a
+				display block
+				overflow hidden
+				width 100%
+				height 100%
+				background-position center
+				background-size cover
+
+	</style>
+	<script>
+		this.image = this.opts.image;
+	</script>
+</mk-images-image>
diff --git a/src/web/app/mobile/tags/index.ts b/src/web/app/mobile/tags/index.ts
index 19952c20c..fd5952ea1 100644
--- a/src/web/app/mobile/tags/index.ts
+++ b/src/web/app/mobile/tags/index.ts
@@ -25,7 +25,7 @@ require('./home-timeline.tag');
 require('./timeline.tag');
 require('./post-preview.tag');
 require('./sub-post-content.tag');
-require('./images-viewer.tag');
+require('./images.tag');
 require('./drive.tag');
 require('./drive-selector.tag');
 require('./drive-folder-selector.tag');
diff --git a/src/web/app/mobile/tags/post-detail.tag b/src/web/app/mobile/tags/post-detail.tag
index 9f212a249..1816d1bf9 100644
--- a/src/web/app/mobile/tags/post-detail.tag
+++ b/src/web/app/mobile/tags/post-detail.tag
@@ -34,7 +34,7 @@
 		<div class="body">
 			<div class="text" ref="text"></div>
 			<div class="media" if={ p.media }>
-				<virtual each={ file in p.media }><img src={ file.url + '?thumbnail&size=512' } alt={ file.name } title={ file.name }/></virtual>
+				<mk-images images={ p.media }/>
 			</div>
 			<mk-poll if={ p.poll } post={ p }/>
 		</div>
diff --git a/src/web/app/mobile/tags/sub-post-content.tag b/src/web/app/mobile/tags/sub-post-content.tag
index 9436b6c1d..adeb84dea 100644
--- a/src/web/app/mobile/tags/sub-post-content.tag
+++ b/src/web/app/mobile/tags/sub-post-content.tag
@@ -2,7 +2,7 @@
 	<div class="body"><a class="reply" if={ post.reply_id }>%fa:reply%</a><span ref="text"></span><a class="quote" if={ post.repost_id } href={ '/post:' + post.repost_id }>RP: ...</a></div>
 	<details if={ post.media }>
 		<summary>({ post.media.length }個のメディア)</summary>
-		<mk-images-viewer images={ post.media }/>
+		<mk-images images={ post.media }/>
 	</details>
 	<details if={ post.poll }>
 		<summary>%i18n:mobile.tags.mk-sub-post-content.poll%</summary>
diff --git a/src/web/app/mobile/tags/timeline.tag b/src/web/app/mobile/tags/timeline.tag
index 19f90a1c1..9e85f97da 100644
--- a/src/web/app/mobile/tags/timeline.tag
+++ b/src/web/app/mobile/tags/timeline.tag
@@ -172,7 +172,7 @@
 					<a class="quote" if={ p.repost != null }>RP:</a>
 				</div>
 				<div class="media" if={ p.media }>
-					<mk-images-viewer images={ p.media }/>
+					<mk-images images={ p.media }/>
 				</div>
 				<mk-poll if={ p.poll } post={ p } ref="pollViewer"/>
 				<span class="app" if={ p.app }>via <b>{ p.app.name }</b></span>

From cd933d9b3f5e1c2a3717877b67f9cad0f4dc9d92 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 10 Dec 2017 22:34:06 +0900
Subject: [PATCH 047/186] v3334

---
 CHANGELOG.md | 4 ++++
 package.json | 2 +-
 2 files changed, 5 insertions(+), 1 deletion(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1274b0fe9..39fc02528 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,10 @@ ChangeLog (Release Notes)
 =========================
 主に notable な changes を書いていきます
 
+3334 (2017/12/10)
+-----------------
+* いい感じにした
+
 3322 (2017/12/10)
 -----------------
 * :art:
diff --git a/package.json b/package.json
index 369113ba6..2b55615cf 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
 	"name": "misskey",
 	"author": "syuilo <i@syuilo.com>",
-	"version": "0.0.3322",
+	"version": "0.0.3334",
 	"license": "MIT",
 	"description": "A miniblog-based SNS",
 	"bugs": "https://github.com/syuilo/misskey/issues",

From d3e5a546d7c7a86967d1ef3cbb3b5fa6432cfa3c Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 11 Dec 2017 02:54:34 +0900
Subject: [PATCH 048/186] :v:

---
 locales/en.yml                                |  7 ++--
 locales/ja.yml                                |  9 +++--
 src/web/app/desktop/tags/settings.tag         | 16 +++++----
 src/web/app/mobile/router.ts                  |  5 ---
 src/web/app/mobile/tags/index.ts              |  1 -
 src/web/app/mobile/tags/page/settings.tag     |  1 -
 src/web/app/mobile/tags/page/settings/api.tag | 36 -------------------
 7 files changed, 21 insertions(+), 54 deletions(-)
 delete mode 100644 src/web/app/mobile/tags/page/settings/api.tag

diff --git a/locales/en.yml b/locales/en.yml
index 8392e170c..8e1dee826 100644
--- a/locales/en.yml
+++ b/locales/en.yml
@@ -199,7 +199,11 @@ ch:
 desktop:
   tags:
     mk-api-info:
-      regenerate-token: "Please enter the password"
+      intro: "APIを利用するには、上記のトークンを「i」というキーでパラメータに付加してリクエストします。"
+      caution: "アカウントを不正利用される可能性があるため、このトークンは第三者に教えないでください(アプリなどにも入力しないでください)。"
+      regeneration-of-token: "万が一このトークンが漏れたりその可能性がある場合はトークンを再生成できます。"
+      regenerate-token: "Regenerate the token"
+      enter-password: "Please enter the password"
 
     mk-drive-browser-base-contextmenu:
       create-folder: "Create a folder"
@@ -524,7 +528,6 @@ mobile:
       applications: "Applications"
       twitter-integration: "Twitter integration"
       signin-history: "Sign in history"
-      api: "API"
       link: "MisskeyLink"
       settings: "Settings"
       signout: "Sign out"
diff --git a/locales/ja.yml b/locales/ja.yml
index f9d41d909..1497bdb6d 100644
--- a/locales/ja.yml
+++ b/locales/ja.yml
@@ -199,7 +199,11 @@ ch:
 desktop:
   tags:
     mk-api-info:
-      regenerate-token: "パスワードを入力してください"
+      intro: "APIを利用するには、上記のトークンを「i」というキーでパラメータに付加してリクエストします。"
+      caution: "アカウントを不正利用される可能性があるため、このトークンは第三者に教えないでください(アプリなどにも入力しないでください)。"
+      regeneration-of-token: "万が一このトークンが漏れたりその可能性がある場合はトークンを再生成できます。"
+      regenerate-token: "トークンを再生成"
+      enter-password: "パスワードを入力してください"
 
     mk-drive-browser-base-contextmenu:
       create-folder: "フォルダーを作成"
@@ -523,8 +527,7 @@ mobile:
       profile: "プロフィール"
       applications: "アプリケーション"
       twitter-integration: "Twitter連携"
-      signin-history: "ログイン履歴"
-      api: "API"
+      signin-history: "サインイン履歴"
       link: "Misskeyリンク"
       settings: "設定"
       signout: "サインアウト"
diff --git a/src/web/app/desktop/tags/settings.tag b/src/web/app/desktop/tags/settings.tag
index f7ecfe3e8..0a9a16250 100644
--- a/src/web/app/desktop/tags/settings.tag
+++ b/src/web/app/desktop/tags/settings.tag
@@ -196,18 +196,22 @@
 </mk-profile-setting>
 
 <mk-api-info>
-	<p>Token:<code>{ I.token }</code></p>
-	<p>APIを利用するには、上記のトークンを「i」というキーでパラメータに付加してリクエストします。</p>
-	<p>アカウントを乗っ取られてしまう可能性があるため、このトークンは第三者に教えないでください(アプリなどにも入力しないでください)。</p>
-	<p>万が一このトークンが漏れたりその可能性がある場合は<a class="regenerate" onclick={ regenerateToken }>トークンを再生成</a>できます。(副作用として、ログインしているすべてのデバイスでログアウトが発生します)</p>
+	<p>Token: <code>{ I.token }</code></p>
+	<p>%i18n:desktop.tags.mk-api-info.intro%</p>
+	<div class="ui info warn"><p>%fa:exclamation-triangle%%i18n:desktop.tags.mk-api-info.caution%</p></div>
+	<p>%i18n:desktop.tags.mk-api-info.regeneration-of-token%</p>
+	<button class="ui" onclick={ regenerateToken }>%i18n:desktop.tags.mk-api-info.regenerate-token%</button>
 	<style>
 		:scope
 			display block
 			color #4a535a
 
 			code
-				padding 4px
+				display inline-block
+				padding 4px 6px
+				color #555
 				background #eee
+				border-radius 2px
 	</style>
 	<script>
 		import passwordDialog from '../scripts/password-dialog';
@@ -216,7 +220,7 @@
 		this.mixin('api');
 
 		this.regenerateToken = () => {
-			passwordDialog('%i18n:desktop.tags.mk-api-info.regenerate-token%', password => {
+			passwordDialog('%i18n:desktop.tags.mk-api-info.enter-password%', password => {
 				this.api('i/regenerate_token', {
 					password: password
 				});
diff --git a/src/web/app/mobile/router.ts b/src/web/app/mobile/router.ts
index 0358d10e9..d0c6add0b 100644
--- a/src/web/app/mobile/router.ts
+++ b/src/web/app/mobile/router.ts
@@ -19,7 +19,6 @@ export default (mios: MiOS) => {
 	route('/i/settings',                 settings);
 	route('/i/settings/profile',         settingsProfile);
 	route('/i/settings/signin-history',  settingsSignin);
-	route('/i/settings/api',             settingsApi);
 	route('/i/settings/twitter',         settingsTwitter);
 	route('/i/settings/authorized-apps', settingsAuthorizedApps);
 	route('/post/new',                   newPost);
@@ -74,10 +73,6 @@ export default (mios: MiOS) => {
 		mount(document.createElement('mk-signin-history-page'));
 	}
 
-	function settingsApi() {
-		mount(document.createElement('mk-api-info-page'));
-	}
-
 	function settingsTwitter() {
 		mount(document.createElement('mk-twitter-setting-page'));
 	}
diff --git a/src/web/app/mobile/tags/index.ts b/src/web/app/mobile/tags/index.ts
index fd5952ea1..20934cdd8 100644
--- a/src/web/app/mobile/tags/index.ts
+++ b/src/web/app/mobile/tags/index.ts
@@ -14,7 +14,6 @@ require('./page/search.tag');
 require('./page/settings.tag');
 require('./page/settings/profile.tag');
 require('./page/settings/signin.tag');
-require('./page/settings/api.tag');
 require('./page/settings/authorized-apps.tag');
 require('./page/settings/twitter.tag');
 require('./page/messaging.tag');
diff --git a/src/web/app/mobile/tags/page/settings.tag b/src/web/app/mobile/tags/page/settings.tag
index 978978214..9a73b0af3 100644
--- a/src/web/app/mobile/tags/page/settings.tag
+++ b/src/web/app/mobile/tags/page/settings.tag
@@ -24,7 +24,6 @@
 		<li><a href="./settings/authorized-apps">%fa:puzzle-piece%%i18n:mobile.tags.mk-settings-page.applications%%fa:angle-right%</a></li>
 		<li><a href="./settings/twitter">%fa:B twitter%%i18n:mobile.tags.mk-settings-page.twitter-integration%%fa:angle-right%</a></li>
 		<li><a href="./settings/signin-history">%fa:sign-in-alt%%i18n:mobile.tags.mk-settings-page.signin-history%%fa:angle-right%</a></li>
-		<li><a href="./settings/api">%fa:key%%i18n:mobile.tags.mk-settings-page.api%%fa:angle-right%</a></li>
 	</ul>
 	<ul>
 		<li><a onclick={ signout }>%fa:power-off%%i18n:mobile.tags.mk-settings-page.signout%</a></li>
diff --git a/src/web/app/mobile/tags/page/settings/api.tag b/src/web/app/mobile/tags/page/settings/api.tag
deleted file mode 100644
index 8de0e9696..000000000
--- a/src/web/app/mobile/tags/page/settings/api.tag
+++ /dev/null
@@ -1,36 +0,0 @@
-<mk-api-info-page>
-	<mk-ui ref="ui">
-		<mk-api-info/>
-	</mk-ui>
-	<style>
-		:scope
-			display block
-	</style>
-	<script>
-		import ui from '../../../scripts/ui-event';
-
-		this.on('mount', () => {
-			document.title = 'Misskey | API';
-			ui.trigger('title', '%fa:key%API');
-		});
-	</script>
-</mk-api-info-page>
-
-<mk-api-info>
-	<p>Token:<code>{ I.token }</code></p>
-	<p>APIを利用するには、上記のトークンを「i」というキーでパラメータに付加してリクエストします。</p>
-	<p>アカウントを乗っ取られてしまう可能性があるため、このトークンは第三者に教えないでください(アプリなどにも入力しないでください)。</p>
-	<p>万が一このトークンが漏れたりその可能性がある場合はデスクトップ版Misskeyから再生成できます。</p>
-	<style>
-		:scope
-			display block
-			color #4a535a
-
-			code
-				padding 4px
-				background #eee
-	</style>
-	<script>
-		this.mixin('i');
-	</script>
-</mk-api-info>

From 711ddc03afce1adc066cdd0704f407954d269f39 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 11 Dec 2017 02:59:05 +0900
Subject: [PATCH 049/186] #984

---
 src/file/server.ts | 1 +
 1 file changed, 1 insertion(+)

diff --git a/src/file/server.ts b/src/file/server.ts
index 990b6ccbe..3bda5b14f 100644
--- a/src/file/server.ts
+++ b/src/file/server.ts
@@ -82,6 +82,7 @@ function thumbnail(data: stream.Readable, type: string, resize: number): ISend {
 	const stream = g
 		.compress('jpeg')
 		.quality(80)
+		.interlace('line')
 		.noProfile() // Remove EXIF
 		.stream();
 

From f849dcb7b91233eaad9b679feef512bb4ad1dcd5 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 11 Dec 2017 03:25:58 +0900
Subject: [PATCH 050/186] #983

---
 locales/en.yml                         |  1 -
 locales/ja.yml                         |  1 -
 src/web/app/desktop/tags/post-form.tag | 69 ++++++++++-------------
 src/web/app/mobile/tags/post-form.tag  | 76 ++++++++++----------------
 4 files changed, 60 insertions(+), 87 deletions(-)

diff --git a/locales/en.yml b/locales/en.yml
index 8e1dee826..9ac9a36cd 100644
--- a/locales/en.yml
+++ b/locales/en.yml
@@ -591,7 +591,6 @@ mobile:
       submit: "Post"
       reply-placeholder: "Reply to this post..."
       post-placeholder: "What's happening?"
-      attach-media-from-local: "Attach media from your device"
 
     mk-search-posts:
       empty: "There is no post related to the 「{}」"
diff --git a/locales/ja.yml b/locales/ja.yml
index 1497bdb6d..2f95998a8 100644
--- a/locales/ja.yml
+++ b/locales/ja.yml
@@ -591,7 +591,6 @@ mobile:
       submit: "投稿"
       reply-placeholder: "この投稿への返信..."
       post-placeholder: "いまどうしてる?"
-      attach-media-from-local: "デバイスからメディアを添付"
 
     mk-search-posts:
       empty: "「{}」に関する投稿は見つかりませんでした。"
diff --git a/src/web/app/desktop/tags/post-form.tag b/src/web/app/desktop/tags/post-form.tag
index 8e5171c83..0b4c07906 100644
--- a/src/web/app/desktop/tags/post-form.tag
+++ b/src/web/app/desktop/tags/post-form.tag
@@ -1,13 +1,12 @@
 <mk-post-form ondragover={ ondragover } ondragenter={ ondragenter } ondragleave={ ondragleave } ondrop={ ondrop }>
 	<div class="content">
 		<textarea class={ with: (files.length != 0 || poll) } ref="text" disabled={ wait } oninput={ update } onkeydown={ onkeydown } onpaste={ onpaste } placeholder={ placeholder }></textarea>
-		<div class="medias { with: poll }" if={ files.length != 0 }>
-			<ul>
-				<li each={ files }>
+		<div class="medias { with: poll }" show={ files.length != 0 }>
+			<ul ref="media">
+				<li each={ files } data-id={ id }>
 					<div class="img" style="background-image: url({ url + '?thumbnail&size=64' })" title={ name }></div>
 					<img class="remove" onclick={ removeFile } src="/assets/desktop/remove.png" title="%i18n:desktop.tags.mk-post-form.attach-cancel%" alt=""/>
 				</li>
-				<li class="add" if={ files.length < 4 } title="%i18n:desktop.tags.mk-post-form.attach-media-from-local%" onclick={ selectFile }>%fa:plus%</li>
 			</ul>
 			<p class="remain">{ 4 - files.length }/4</p>
 		</div>
@@ -118,8 +117,9 @@
 						> li
 							display block
 							float left
-							margin 4px
+							margin 0
 							padding 0
+							border solid 4px transparent
 							cursor move
 
 							&:hover > .remove
@@ -140,29 +140,6 @@
 								height 16px
 								cursor pointer
 
-						> .add
-							display block
-							float left
-							margin 4px
-							padding 0
-							border dashed 2px rgba($theme-color, 0.2)
-							cursor pointer
-
-							&:hover
-								border-color rgba($theme-color, 0.3)
-
-								> i
-									color rgba($theme-color, 0.4)
-
-							> i
-								display block
-								width 60px
-								height 60px
-								line-height 60px
-								text-align center
-								font-size 1.2em
-								color rgba($theme-color, 0.2)
-
 				> mk-poll-editor
 					background lighten($theme-color, 98%)
 					border solid 1px rgba($theme-color, 0.1)
@@ -306,6 +283,7 @@
 
 	</style>
 	<script>
+		import Sortable from 'sortablejs';
 		import getKao from '../../common/scripts/get-kao';
 		import notify from '../scripts/notify';
 		import Autocomplete from '../scripts/autocomplete';
@@ -365,6 +343,10 @@
 				this.trigger('change-files', this.files);
 				this.update();
 			}
+
+			new Sortable(this.refs.media, {
+				animation: 150
+			});
 		});
 
 		this.on('unmount', () => {
@@ -413,14 +395,17 @@
 			const data = e.dataTransfer.getData('text');
 			if (data == null) return false;
 
-			// パース
-			// TODO: Validate JSON
-			const obj = JSON.parse(data);
+			try {
+				// パース
+				const obj = JSON.parse(data);
+
+				// (ドライブの)ファイルだったら
+				if (obj.type == 'file') {
+					this.files.push(obj.file);
+					this.update();
+				}
+			} catch (e) {
 
-			// (ドライブの)ファイルだったら
-			if (obj.type == 'file') {
-				this.files.push(obj.file);
-				this.update();
 			}
 		};
 
@@ -483,13 +468,19 @@
 		this.post = e => {
 			this.wait = true;
 
-			const files = this.files && this.files.length > 0
-				? this.files.map(f => f.id)
-				: undefined;
+			const files = [];
+
+			if (this.files.length > 0) {
+				Array.from(this.refs.media.children).forEach(el => {
+					const id = el.getAttribute('data-id');
+					const file = this.files.find(f => f.id == id);
+					files.push(file);
+				});
+			}
 
 			this.api('posts/create', {
 				text: this.refs.text.value == '' ? undefined : this.refs.text.value,
-				media_ids: files,
+				media_ids: this.files.length > 0 ? files.map(f => f.id) : undefined,
 				reply_id: this.inReplyToPost ? this.inReplyToPost.id : undefined,
 				repost_id: this.repost ? this.repost.id : undefined,
 				poll: this.poll ? this.refs.poll.get() : undefined
diff --git a/src/web/app/mobile/tags/post-form.tag b/src/web/app/mobile/tags/post-form.tag
index 3ac7296f7..f09f40bb5 100644
--- a/src/web/app/mobile/tags/post-form.tag
+++ b/src/web/app/mobile/tags/post-form.tag
@@ -9,12 +9,11 @@
 	<div class="form">
 		<mk-post-preview if={ opts.reply } post={ opts.reply }/>
 		<textarea ref="text" disabled={ wait } oninput={ update } onkeydown={ onkeydown } onpaste={ onpaste } placeholder={ opts.reply ? '%i18n:mobile.tags.mk-post-form.reply-placeholder%' : '%i18n:mobile.tags.mk-post-form.post-placeholder%' }></textarea>
-		<div class="attaches" if={ files.length != 0 }>
+		<div class="attaches" show={ files.length != 0 }>
 			<ul class="files" ref="attaches">
-				<li class="file" each={ files }>
-					<div class="img" style="background-image: url({ url + '?thumbnail&size=64' })" title={ name }></div>
+				<li class="file" each={ files } data-id={ id }>
+					<div class="img" style="background-image: url({ url + '?thumbnail&size=64' })" title={ name } onclick={ removeFile }></div>
 				</li>
-				<li class="add" if={ files.length < 4 } title="%i18n:mobile.tags.mk-post-form.attach-media-from-local%" onclick={ selectFile }>%fa:plus%</li>
 			</ul>
 		</div>
 		<mk-poll-editor if={ poll } ref="poll" ondestroy={ onPollDestroyed }/>
@@ -93,12 +92,9 @@
 						> .file
 							display block
 							float left
-							margin 4px
+							margin 0
 							padding 0
-							cursor move
-
-							&:hover > .remove
-								display block
+							border solid 4px transparent
 
 							> .img
 								width 64px
@@ -106,38 +102,6 @@
 								background-size cover
 								background-position center center
 
-							> .remove
-								display none
-								position absolute
-								top -6px
-								right -6px
-								width 16px
-								height 16px
-								cursor pointer
-
-						> .add
-							display block
-							float left
-							margin 4px
-							padding 0
-							border dashed 2px rgba($theme-color, 0.2)
-							cursor pointer
-
-							&:hover
-								border-color rgba($theme-color, 0.3)
-
-								> [data-fa]
-									color rgba($theme-color, 0.4)
-
-							> [data-fa]
-								display block
-								width 60px
-								height 60px
-								line-height 60px
-								text-align center
-								font-size 1.2em
-								color rgba($theme-color, 0.2)
-
 				> mk-uploader
 					margin 8px 0 0 0
 					padding 8px
@@ -181,6 +145,7 @@
 
 	</style>
 	<script>
+		import Sortable from 'sortablejs';
 		import getKao from '../../common/scripts/get-kao';
 
 		this.mixin('api');
@@ -200,6 +165,10 @@
 			});
 
 			this.refs.text.focus();
+
+			new Sortable(this.refs.attaches, {
+				animation: 150
+			});
 		});
 
 		this.onkeydown = e => {
@@ -247,6 +216,13 @@
 			this.update();
 		};
 
+		this.removeFile = e => {
+			const file = e.item;
+			this.files = this.files.filter(x => x.id != file.id);
+			this.trigger('change-files', this.files);
+			this.update();
+		};
+
 		this.addPoll = () => {
 			this.poll = true;
 		};
@@ -258,15 +234,23 @@
 		};
 
 		this.post = () => {
-			this.wait = true;
+			this.update({
+				wait: true
+			});
 
-			const files = this.files && this.files.length > 0
-				? this.files.map(f => f.id)
-				: undefined;
+			const files = [];
+
+			if (this.files.length > 0) {
+				Array.from(this.refs.attaches.children).forEach(el => {
+					const id = el.getAttribute('data-id');
+					const file = this.files.find(f => f.id == id);
+					files.push(file);
+				});
+			}
 
 			this.api('posts/create', {
 				text: this.refs.text.value == '' ? undefined : this.refs.text.value,
-				media_ids: files,
+				media_ids: this.files.length > 0 ? files.map(f => f.id) : undefined,
 				reply_id: opts.reply ? opts.reply.id : undefined,
 				poll: this.poll ? this.refs.poll.get() : undefined
 			}).then(data => {

From 845dc261846661359d38fccea0f84b99ed6968e5 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 11 Dec 2017 03:38:31 +0900
Subject: [PATCH 051/186] :v:

---
 locales/en.yml                        |  2 ++
 locales/ja.yml                        |  2 ++
 src/web/app/desktop/tags/settings.tag | 27 ++++++++-------------------
 webpack/module/rules/index.ts         |  2 ++
 webpack/module/rules/license.ts       | 22 ++++++++++++++++++++++
 5 files changed, 36 insertions(+), 19 deletions(-)
 create mode 100644 webpack/module/rules/license.ts

diff --git a/locales/en.yml b/locales/en.yml
index 9ac9a36cd..b49af68bd 100644
--- a/locales/en.yml
+++ b/locales/en.yml
@@ -365,6 +365,8 @@ desktop:
       security: "Security"
       password: "Password"
       2fa: "Two-factor authentication"
+      other: "Other"
+      license: "License"
 
     mk-timeline-post:
       reposted-by: "Reposted by {}"
diff --git a/locales/ja.yml b/locales/ja.yml
index 2f95998a8..afafa5a63 100644
--- a/locales/ja.yml
+++ b/locales/ja.yml
@@ -365,6 +365,8 @@ desktop:
       security: "セキュリティ"
       password: "パスワード"
       2fa: "二段階認証"
+      other: "その他"
+      license: "ライセンス"
 
     mk-timeline-post:
       reposted-by: "{}がRepost"
diff --git a/src/web/app/desktop/tags/settings.tag b/src/web/app/desktop/tags/settings.tag
index 0a9a16250..2f36d9b3e 100644
--- a/src/web/app/desktop/tags/settings.tag
+++ b/src/web/app/desktop/tags/settings.tag
@@ -8,6 +8,7 @@
 		<p class={ active: page == 'twitter' } onmousedown={ setPage.bind(null, 'twitter') }>%fa:B twitter .fw%Twitter</p>
 		<p class={ active: page == 'security' } onmousedown={ setPage.bind(null, 'security') }>%fa:unlock-alt .fw%%i18n:desktop.tags.mk-settings.security%</p>
 		<p class={ active: page == 'api' } onmousedown={ setPage.bind(null, 'api') }>%fa:key .fw%API</p>
+		<p class={ active: page == 'other' } onmousedown={ setPage.bind(null, 'other') }>%fa:cogs .fw%%i18n:desktop.tags.mk-settings.other%</p>
 	</div>
 	<div class="pages">
 		<section class="profile" show={ page == 'profile' }>
@@ -54,6 +55,11 @@
 			<h1>API</h1>
 			<mk-api-info/>
 		</section>
+
+		<section class="other" show={ page == 'other' }>
+			<h1>%i18n:desktop.tags.mk-settings.license%</h1>
+			%license%
+		</section>
 	</div>
 	<style>
 		:scope
@@ -96,8 +102,9 @@
 
 				> section
 					margin 32px
+					color #4a535a
 
-					h1
+					> h1
 						display block
 						margin 0 0 1em 0
 						padding 0 0 8px 0
@@ -105,24 +112,6 @@
 						color #555
 						border-bottom solid 1px #eee
 
-					label.checkbox
-						> input
-							position absolute
-							top 0
-							left 0
-
-							&:checked + p
-								color $theme-color
-
-						> p
-							width calc(100% - 32px)
-							margin 0 0 0 32px
-							font-weight bold
-
-							&:last-child
-								font-weight normal
-								color #999
-
 	</style>
 	<script>
 		this.page = 'profile';
diff --git a/webpack/module/rules/index.ts b/webpack/module/rules/index.ts
index 79740ce48..b6a0a5e2e 100644
--- a/webpack/module/rules/index.ts
+++ b/webpack/module/rules/index.ts
@@ -1,4 +1,5 @@
 import i18n from './i18n';
+import license from './license';
 import fa from './fa';
 import base64 from './base64';
 import themeColor from './theme-color';
@@ -8,6 +9,7 @@ import typescript from './typescript';
 
 export default (lang, locale) => [
 	i18n(lang, locale),
+	license(),
 	fa(),
 	base64(),
 	themeColor(),
diff --git a/webpack/module/rules/license.ts b/webpack/module/rules/license.ts
new file mode 100644
index 000000000..1795af960
--- /dev/null
+++ b/webpack/module/rules/license.ts
@@ -0,0 +1,22 @@
+/**
+ * Inject license
+ */
+
+import * as fs from 'fs';
+const StringReplacePlugin = require('string-replace-webpack-plugin');
+
+const license = fs.readFileSync(__dirname + '/../../../LICENSE', 'utf-8')
+	.replace(/\r\n/g, '\n')
+	.replace(/(.)\n(.)/g, '$1 $2')
+	.replace(/(^|\n)(.*?)($|\n)/g, '<p>$2</p>');
+
+export default () => ({
+	enforce: 'pre',
+	test: /\.(tag|js)$/,
+	exclude: /node_modules/,
+	loader: StringReplacePlugin.replace({
+		replacements: [{
+			pattern: '%license%', replacement: () => license
+		}]
+	})
+});

From ad613ec5197a5476c7a38112487747c6045e3258 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 11 Dec 2017 03:39:17 +0900
Subject: [PATCH 052/186] v3339

---
 CHANGELOG.md | 4 ++++
 package.json | 2 +-
 2 files changed, 5 insertions(+), 1 deletion(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 39fc02528..d25107389 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,10 @@ ChangeLog (Release Notes)
 =========================
 主に notable な changes を書いていきます
 
+3339 (2017/12/11)
+-----------------
+* なんか
+
 3334 (2017/12/10)
 -----------------
 * いい感じにした
diff --git a/package.json b/package.json
index 2b55615cf..f03446531 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
 	"name": "misskey",
 	"author": "syuilo <i@syuilo.com>",
-	"version": "0.0.3334",
+	"version": "0.0.3339",
 	"license": "MIT",
 	"description": "A miniblog-based SNS",
 	"bugs": "https://github.com/syuilo/misskey/issues",

From 9852c57b8f5cf4d33dc1b363e14a06f46954f7fd Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 11 Dec 2017 12:20:36 +0900
Subject: [PATCH 053/186] Fix bug

---
 gulpfile.ts | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/gulpfile.ts b/gulpfile.ts
index 641500bbe..ee11a02dc 100644
--- a/gulpfile.ts
+++ b/gulpfile.ts
@@ -32,6 +32,7 @@ fontawesome.library.add(solid);
 fontawesome.library.add(brands);
 
 import version from './src/version';
+import config from './src/conf';
 
 const uglify = uglifyComposer(uglifyes, console);
 
@@ -142,6 +143,7 @@ gulp.task('webpack', done => {
 gulp.task('build:client:script', () =>
 	gulp.src(['./src/web/app/boot.js', './src/web/app/safe.js'])
 		.pipe(replace('VERSION', JSON.stringify(version)))
+		.pipe(replace('API', JSON.stringify(config.api_url)))
 		.pipe(isProduction ? uglify({
 			toplevel: true
 		} as any) : gutil.noop())

From e36a7081324b9e538ae40918072edd93ebc9b2cb Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 11 Dec 2017 13:33:33 +0900
Subject: [PATCH 054/186] #986

---
 src/api/common/add-file-to-drive.ts           | 39 ++++++++++-
 src/api/endpoints/drive/files/create.ts       | 14 ++--
 src/web/app/desktop/tags/drive/file.tag       | 16 ++++-
 src/web/app/desktop/tags/images.tag           | 13 +++-
 src/web/app/mobile/tags/drive/file-viewer.tag |  7 +-
 src/web/app/mobile/tags/drive/file.tag        |  6 +-
 src/web/app/mobile/tags/images.tag            |  6 +-
 tools/migration/node.2017-12-11.js            | 67 +++++++++++++++++++
 8 files changed, 157 insertions(+), 11 deletions(-)
 create mode 100644 tools/migration/node.2017-12-11.js

diff --git a/src/api/common/add-file-to-drive.ts b/src/api/common/add-file-to-drive.ts
index 109e88610..427b54d72 100644
--- a/src/api/common/add-file-to-drive.ts
+++ b/src/api/common/add-file-to-drive.ts
@@ -110,7 +110,7 @@ const addFile = async (
 		}
 	}
 
-	const [wh, folder] = await Promise.all([
+	const [wh, averageColor, folder] = await Promise.all([
 		// Width and height (when image)
 		(async () => {
 			// 画像かどうか
@@ -125,14 +125,45 @@ const addFile = async (
 				return null;
 			}
 
+			log('calculate image width and height...');
+
 			// Calculate width and height
 			const g = gm(fs.createReadStream(path), name);
 			const size = await prominence(g).size();
 
-			log('image width and height is calculated');
+			log(`image width and height is calculated: ${size.width}, ${size.height}`);
 
 			return [size.width, size.height];
 		})(),
+		// average color (when image)
+		(async () => {
+			// 画像かどうか
+			if (!/^image\/.*$/.test(mime)) {
+				return null;
+			}
+
+			const imageType = mime.split('/')[1];
+
+			// 画像でもPNGかJPEGでないならスキップ
+			if (imageType != 'png' && imageType != 'jpeg') {
+				return null;
+			}
+
+			log('calculate average color...');
+
+			const buffer = await prominence(gm(fs.createReadStream(path), name)
+				.setFormat('ppm')
+				.resize(1, 1)) // 1pxのサイズに縮小して平均色を取得するというハック
+				.toBuffer();
+
+			const r = buffer.readUInt8(buffer.length - 3);
+			const g = buffer.readUInt8(buffer.length - 2);
+			const b = buffer.readUInt8(buffer.length - 1);
+
+			log(`average color is calculated: ${r}, ${g}, ${b}`);
+
+			return [r, g, b];
+		})(),
 		// folder
 		(async () => {
 			if (!folderId) {
@@ -188,6 +219,10 @@ const addFile = async (
 		properties['height'] = wh[1];
 	}
 
+	if (averageColor) {
+		properties['average_color'] = averageColor;
+	}
+
 	return addToGridFS(detectedName, readable, mime, {
 		user_id: user._id,
 		folder_id: folder !== null ? folder._id : null,
diff --git a/src/api/endpoints/drive/files/create.ts b/src/api/endpoints/drive/files/create.ts
index 7546eca30..437348a1e 100644
--- a/src/api/endpoints/drive/files/create.ts
+++ b/src/api/endpoints/drive/files/create.ts
@@ -38,9 +38,15 @@ module.exports = async (file, params, user): Promise<any> => {
 	const [folderId = null, folderIdErr] = $(params.folder_id).optional.nullable.id().$;
 	if (folderIdErr) throw 'invalid folder_id param';
 
-	// Create file
-	const driveFile = await create(user, file.path, name, null, folderId);
+	try {
+		// Create file
+		const driveFile = await create(user, file.path, name, null, folderId);
 
-	// Serialize
-	return serialize(driveFile);
+		// Serialize
+		return serialize(driveFile);
+	} catch (e) {
+		console.error(e);
+
+		throw e;
+	}
 };
diff --git a/src/web/app/desktop/tags/drive/file.tag b/src/web/app/desktop/tags/drive/file.tag
index 0f019d95b..8b3d36b3f 100644
--- a/src/web/app/desktop/tags/drive/file.tag
+++ b/src/web/app/desktop/tags/drive/file.tag
@@ -5,7 +5,9 @@
 	<div class="label" if={ I.banner_id == file.id }><img src="/assets/label.svg"/>
 		<p>%i18n:desktop.tags.mk-drive-browser-file.banner%</p>
 	</div>
-	<div class="thumbnail"><img src={ file.url + '?thumbnail&size=128' } alt=""/></div>
+	<div class="thumbnail" ref="thumbnail" style="background-color:{ file.properties.average_color ? 'rgb(' + file.properties.average_color.join(',') + ')' : 'transparent' }">
+		<img src={ file.url + '?thumbnail&size=128' } alt="" onload={ onload }/>
+	</div>
 	<p class="name"><span>{ file.name.lastIndexOf('.') != -1 ? file.name.substr(0, file.name.lastIndexOf('.')) : file.name }</span><span class="ext" if={ file.name.lastIndexOf('.') != -1 }>{ file.name.substr(file.name.lastIndexOf('.')) }</span></p>
 	<style>
 		:scope
@@ -139,6 +141,7 @@
 
 	</style>
 	<script>
+		import anime from 'animejs';
 		import bytesToSize from '../../../common/scripts/bytes-to-size';
 
 		this.mixin('i');
@@ -199,5 +202,16 @@
 			this.isDragging = false;
 			this.browser.isDragSource = false;
 		};
+
+		this.onload = () => {
+			if (this.file.properties.average_color) {
+				anime({
+					targets: this.refs.thumbnail,
+					backgroundColor: `rgba(${this.file.properties.average_color.join(',')}, 0)`,
+					duration: 100,
+					easing: 'linear'
+				});
+			}
+		};
 	</script>
 </mk-drive-browser-file>
diff --git a/src/web/app/desktop/tags/images.tag b/src/web/app/desktop/tags/images.tag
index ce67d26a9..5e4be481d 100644
--- a/src/web/app/desktop/tags/images.tag
+++ b/src/web/app/desktop/tags/images.tag
@@ -53,7 +53,13 @@
 </mk-images>
 
 <mk-images-image>
-	<a ref="view" href={ image.url } onmousemove={ mousemove } onmouseleave={ mouseleave } style={ 'background-image: url(' + image.url + '?thumbnail&size=512' } onclick={ click } title={ image.name }></a>
+	<a ref="view"
+		href={ image.url }
+		onmousemove={ mousemove }
+		onmouseleave={ mouseleave }
+		style={ styles }
+		onclick={ click }
+		title={ image.name }></a>
 	<style>
 		:scope
 			display block
@@ -74,6 +80,11 @@
 	</style>
 	<script>
 		this.image = this.opts.image;
+		this.styles = {
+			'background-color': `rgb(${this.image.properties.average_color.join(',')})`,
+			'background-image': `url(${this.image.url}?thumbnail&size=512)`
+		};
+		console.log(this.styles);
 
 		this.mousemove = e => {
 			const rect = this.refs.view.getBoundingClientRect();
diff --git a/src/web/app/mobile/tags/drive/file-viewer.tag b/src/web/app/mobile/tags/drive/file-viewer.tag
index 48fc83fa6..259873d95 100644
--- a/src/web/app/mobile/tags/drive/file-viewer.tag
+++ b/src/web/app/mobile/tags/drive/file-viewer.tag
@@ -1,6 +1,11 @@
 <mk-drive-file-viewer>
 	<div class="preview">
-		<img if={ kind == 'image' } src={ file.url } alt={ file.name } title={ file.name } onload={ onImageLoaded } ref="img">
+		<img if={ kind == 'image' } ref="img"
+			src={ file.url }
+			alt={ file.name }
+			title={ file.name }
+			onload={ onImageLoaded }
+			style="background-color:rgb({ file.properties.average_color.join(',') })">
 		<virtual if={ kind != 'image' }>%fa:file%</virtual>
 		<footer if={ kind == 'image' && file.properties && file.properties.width && file.properties.height }>
 			<span class="size">
diff --git a/src/web/app/mobile/tags/drive/file.tag b/src/web/app/mobile/tags/drive/file.tag
index 196dd1141..684df7dd0 100644
--- a/src/web/app/mobile/tags/drive/file.tag
+++ b/src/web/app/mobile/tags/drive/file.tag
@@ -1,7 +1,7 @@
 <mk-drive-file data-is-selected={ isSelected }>
 	<a onclick={ onclick } href="/i/drive/file/{ file.id }">
 		<div class="container">
-			<div class="thumbnail" style={ 'background-image: url(' + file.url + '?thumbnail&size=128)' }></div>
+			<div class="thumbnail" style={ thumbnail }></div>
 			<div class="body">
 				<p class="name"><span>{ file.name.lastIndexOf('.') != -1 ? file.name.substr(0, file.name.lastIndexOf('.')) : file.name }</span><span class="ext" if={ file.name.lastIndexOf('.') != -1 }>{ file.name.substr(file.name.lastIndexOf('.')) }</span></p>
 				<!--
@@ -132,6 +132,10 @@
 
 		this.browser = this.parent;
 		this.file = this.opts.file;
+		this.thumbnail = {
+			'background-color': this.file.properties.average_color ? `rgb(${this.file.properties.average_color.join(',')})` : 'transparent',
+			'background-image': `url(${this.file.url}?thumbnail&size=128)`
+		};
 		this.isSelected = this.browser.selectedFiles.some(f => f.id == this.file.id);
 
 		this.browser.on('change-selection', selections => {
diff --git a/src/web/app/mobile/tags/images.tag b/src/web/app/mobile/tags/images.tag
index aaa80e4fd..b200eefe7 100644
--- a/src/web/app/mobile/tags/images.tag
+++ b/src/web/app/mobile/tags/images.tag
@@ -56,7 +56,7 @@
 </mk-images>
 
 <mk-images-image>
-	<a ref="view" href={ image.url } target="_blank" style={ 'background-image: url(' + image.url + '?thumbnail&size=512' } title={ image.name }></a>
+	<a ref="view" href={ image.url } target="_blank" style={ styles } title={ image.name }></a>
 	<style>
 		:scope
 			display block
@@ -74,5 +74,9 @@
 	</style>
 	<script>
 		this.image = this.opts.image;
+		this.styles = {
+			'background-color': `rgb(${this.image.properties.average_color.join(',')})`,
+			'background-image': `url(${this.image.url}?thumbnail&size=512)`
+		};
 	</script>
 </mk-images-image>
diff --git a/tools/migration/node.2017-12-11.js b/tools/migration/node.2017-12-11.js
new file mode 100644
index 000000000..3a3fef051
--- /dev/null
+++ b/tools/migration/node.2017-12-11.js
@@ -0,0 +1,67 @@
+// for Node.js interpret
+
+const { default: DriveFile, getGridFSBucket } = require('../../built/api/models/drive-file')
+const { default: zip } = require('@prezzemolo/zip')
+
+const _gm = require('gm');
+const gm = _gm.subClass({
+	imageMagick: true
+});
+
+const migrate = doc => new Promise(async (res, rej) => {
+	const bucket = await getGridFSBucket();
+
+	const readable = bucket.openDownloadStream(doc._id);
+
+	gm(readable)
+		.setFormat('ppm')
+		.resize(1, 1)
+		.toBuffer(async (err, buffer) => {
+			if (err) rej(err);
+			const r = buffer.readUInt8(buffer.length - 3);
+			const g = buffer.readUInt8(buffer.length - 2);
+			const b = buffer.readUInt8(buffer.length - 1);
+
+			const result = await DriveFile.update(doc._id, {
+				$set: {
+					'metadata.properties.average_color': [r, g, b]
+				}
+			})
+
+			res(result.ok === 1);
+		});
+});
+
+async function main() {
+	const query = {
+		contentType: {
+			$in: [
+				'image/png',
+				'image/jpeg'
+			]
+		}
+	}
+
+	const count = await DriveFile.count(query);
+
+	const dop = Number.parseInt(process.argv[2]) || 5
+	const idop = ((count - (count % dop)) / dop) + 1
+
+	return zip(
+		1,
+		async (time) => {
+			console.log(`${time} / ${idop}`)
+			const doc = await DriveFile.find(query, {
+				limit: dop, skip: time * dop
+			})
+			return Promise.all(doc.map(migrate))
+		},
+		idop
+	).then(a => {
+		const rv = []
+		a.forEach(e => rv.push(...e))
+		return rv
+	})
+}
+
+main().then(console.dir).catch(console.error)

From 731591082f80c66b9f7a179a5cd6e2c3ee0902c2 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 11 Dec 2017 13:34:29 +0900
Subject: [PATCH 055/186] v3342

---
 CHANGELOG.md | 4 ++++
 package.json | 2 +-
 2 files changed, 5 insertions(+), 1 deletion(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index d25107389..d2525b16c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,10 @@ ChangeLog (Release Notes)
 =========================
 主に notable な changes を書いていきます
 
+3342 (2017/12/11)
+-----------------
+* なんか
+
 3339 (2017/12/11)
 -----------------
 * なんか
diff --git a/package.json b/package.json
index f03446531..84d694d2a 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
 	"name": "misskey",
 	"author": "syuilo <i@syuilo.com>",
-	"version": "0.0.3339",
+	"version": "0.0.3342",
 	"license": "MIT",
 	"description": "A miniblog-based SNS",
 	"bugs": "https://github.com/syuilo/misskey/issues",

From 2810fbd3a05863be5a96ca98d1994835e25c5e16 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 11 Dec 2017 13:34:36 +0900
Subject: [PATCH 056/186] Fix bug

---
 .../node.1510016282.change-gridfs-metadata-name-to-filename.js  | 2 +-
 tools/migration/node.1510056272.issue_882.js                    | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/tools/migration/node.1510016282.change-gridfs-metadata-name-to-filename.js b/tools/migration/node.1510016282.change-gridfs-metadata-name-to-filename.js
index 9128d852c..d7b2a6eff 100644
--- a/tools/migration/node.1510016282.change-gridfs-metadata-name-to-filename.js
+++ b/tools/migration/node.1510016282.change-gridfs-metadata-name-to-filename.js
@@ -34,7 +34,7 @@ async function main () {
 		1,
 		async (time) => {
 			console.log(`${time} / ${idop}`)
-			const doc = await db.get('drive_files').find(query, {
+			const doc = await DriveFile.find(query, {
 				limit: dop, skip: time * dop
 			})
 			return Promise.all(doc.map(applyNewChange))
diff --git a/tools/migration/node.1510056272.issue_882.js b/tools/migration/node.1510056272.issue_882.js
index aa1141325..302ef3de6 100644
--- a/tools/migration/node.1510056272.issue_882.js
+++ b/tools/migration/node.1510056272.issue_882.js
@@ -31,7 +31,7 @@ async function main() {
 		1,
 		async (time) => {
 			console.log(`${time} / ${idop}`)
-			const doc = await db.get('drive_files').find(query, {
+			const doc = await DriveFile.find(query, {
 				limit: dop, skip: time * dop
 			})
 			return Promise.all(doc.map(migrate))

From df43de048793fdda20379a0e6c0da6e81a49690d Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 11 Dec 2017 13:52:06 +0900
Subject: [PATCH 057/186] :v:

---
 tools/migration/node.2017-12-11.js | 6 +++++-
 1 file changed, 5 insertions(+), 1 deletion(-)

diff --git a/tools/migration/node.2017-12-11.js b/tools/migration/node.2017-12-11.js
index 3a3fef051..b9686b8b4 100644
--- a/tools/migration/node.2017-12-11.js
+++ b/tools/migration/node.2017-12-11.js
@@ -17,7 +17,11 @@ const migrate = doc => new Promise(async (res, rej) => {
 		.setFormat('ppm')
 		.resize(1, 1)
 		.toBuffer(async (err, buffer) => {
-			if (err) rej(err);
+			if (err) {
+				console.error(err);
+				res(false);
+				return;
+			}
 			const r = buffer.readUInt8(buffer.length - 3);
 			const g = buffer.readUInt8(buffer.length - 2);
 			const b = buffer.readUInt8(buffer.length - 1);

From b83fc7989cf34c4764f7f5b14969707bd78c6ee2 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 11 Dec 2017 14:07:51 +0900
Subject: [PATCH 058/186] :v:

---
 src/api/serializers/drive-file.ts | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/src/api/serializers/drive-file.ts b/src/api/serializers/drive-file.ts
index 92a9492d8..003e09ee7 100644
--- a/src/api/serializers/drive-file.ts
+++ b/src/api/serializers/drive-file.ts
@@ -56,6 +56,8 @@ export default (
 
 	_target.url = `${config.drive_url}/${_target.id}/${encodeURIComponent(_target.name)}`;
 
+	if (_target.properties == null) _target.properties = {};
+
 	if (opts.detail) {
 		if (_target.folder_id) {
 			// Populate folder

From 82ab9c5d1c8d2cc66978633ecefd14adf1de877a Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 11 Dec 2017 14:19:51 +0900
Subject: [PATCH 059/186] Fix bug

---
 src/web/app/desktop/tags/images.tag | 2 +-
 src/web/app/mobile/tags/images.tag  | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/web/app/desktop/tags/images.tag b/src/web/app/desktop/tags/images.tag
index 5e4be481d..eeaf4cd3d 100644
--- a/src/web/app/desktop/tags/images.tag
+++ b/src/web/app/desktop/tags/images.tag
@@ -81,7 +81,7 @@
 	<script>
 		this.image = this.opts.image;
 		this.styles = {
-			'background-color': `rgb(${this.image.properties.average_color.join(',')})`,
+			'background-color': this.image.properties.average_color ? `rgb(${this.image.properties.average_color.join(',')})` : 'transparent',
 			'background-image': `url(${this.image.url}?thumbnail&size=512)`
 		};
 		console.log(this.styles);
diff --git a/src/web/app/mobile/tags/images.tag b/src/web/app/mobile/tags/images.tag
index b200eefe7..5899364ae 100644
--- a/src/web/app/mobile/tags/images.tag
+++ b/src/web/app/mobile/tags/images.tag
@@ -75,7 +75,7 @@
 	<script>
 		this.image = this.opts.image;
 		this.styles = {
-			'background-color': `rgb(${this.image.properties.average_color.join(',')})`,
+			'background-color': this.image.properties.average_color ? `rgb(${this.image.properties.average_color.join(',')})` : 'transparent',
 			'background-image': `url(${this.image.url}?thumbnail&size=512)`
 		};
 	</script>

From 2b5bad8d56b3d422f3cc2b24ddff3cd6c9d07b23 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 11 Dec 2017 14:20:19 +0900
Subject: [PATCH 060/186] v3347

---
 CHANGELOG.md | 4 ++++
 package.json | 2 +-
 2 files changed, 5 insertions(+), 1 deletion(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index d2525b16c..9ab4698ab 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,10 @@ ChangeLog (Release Notes)
 =========================
 主に notable な changes を書いていきます
 
+3347 (2017/12/11)
+-----------------
+* バグ修正
+
 3342 (2017/12/11)
 -----------------
 * なんか
diff --git a/package.json b/package.json
index 84d694d2a..db3b08d31 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
 	"name": "misskey",
 	"author": "syuilo <i@syuilo.com>",
-	"version": "0.0.3342",
+	"version": "0.0.3347",
 	"license": "MIT",
 	"description": "A miniblog-based SNS",
 	"bugs": "https://github.com/syuilo/misskey/issues",

From 256c3c2abd69a0e04dcc8d31fd3bb5d933b68c3c Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 11 Dec 2017 16:43:35 +0900
Subject: [PATCH 061/186] Fix bug

---
 src/api/private/signin.ts | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/src/api/private/signin.ts b/src/api/private/signin.ts
index 7376921e2..a26c8f6c5 100644
--- a/src/api/private/signin.ts
+++ b/src/api/private/signin.ts
@@ -6,8 +6,10 @@ import Signin from '../models/signin';
 import serialize from '../serializers/signin';
 import event from '../event';
 import signin from '../common/signin';
+import config from '../../conf';
 
 export default async (req: express.Request, res: express.Response) => {
+	res.header('Access-Control-Allow-Origin', config.url);
 	res.header('Access-Control-Allow-Credentials', 'true');
 
 	const username = req.body['username'];

From 2c2764cd058649ef2097edcbd4385127ec83a664 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 11 Dec 2017 16:46:38 +0900
Subject: [PATCH 062/186] Clean up

---
 src/web/app/desktop/tags/images.tag | 1 -
 1 file changed, 1 deletion(-)

diff --git a/src/web/app/desktop/tags/images.tag b/src/web/app/desktop/tags/images.tag
index eeaf4cd3d..29540747f 100644
--- a/src/web/app/desktop/tags/images.tag
+++ b/src/web/app/desktop/tags/images.tag
@@ -84,7 +84,6 @@
 			'background-color': this.image.properties.average_color ? `rgb(${this.image.properties.average_color.join(',')})` : 'transparent',
 			'background-image': `url(${this.image.url}?thumbnail&size=512)`
 		};
-		console.log(this.styles);
 
 		this.mousemove = e => {
 			const rect = this.refs.view.getBoundingClientRect();

From f5b56c9e6e7fe328dbbccfd2e09843639e72f5f6 Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Mon, 11 Dec 2017 19:48:29 +0000
Subject: [PATCH 063/186] fix(package): update @types/mongodb to version 2.2.17

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index db3b08d31..d8bce1b99 100644
--- a/package.json
+++ b/package.json
@@ -54,7 +54,7 @@
 		"@types/is-url": "1.2.28",
 		"@types/js-yaml": "3.10.1",
 		"@types/mocha": "2.2.44",
-		"@types/mongodb": "2.2.16",
+		"@types/mongodb": "2.2.17",
 		"@types/monk": "1.0.6",
 		"@types/morgan": "1.7.35",
 		"@types/ms": "0.7.30",

From e9eb76e8fb946923b2df775da4426428645505d5 Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Mon, 11 Dec 2017 20:01:55 +0000
Subject: [PATCH 064/186] fix(package): update @types/node to version 8.0.58

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index db3b08d31..0ee1078a8 100644
--- a/package.json
+++ b/package.json
@@ -59,7 +59,7 @@
 		"@types/morgan": "1.7.35",
 		"@types/ms": "0.7.30",
 		"@types/multer": "1.3.6",
-		"@types/node": "8.0.57",
+		"@types/node": "8.0.58",
 		"@types/page": "1.5.32",
 		"@types/proxy-addr": "2.0.0",
 		"@types/qrcode": "0.8.0",

From b3fef027f8a6df61c9a15dd95bbe0d0a8becbd08 Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Tue, 12 Dec 2017 00:24:25 +0000
Subject: [PATCH 065/186] fix(package): update @types/chai to version 4.0.10

Closes #988
---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index db3b08d31..ef795895c 100644
--- a/package.json
+++ b/package.json
@@ -30,7 +30,7 @@
 		"@prezzemolo/zip": "0.0.3",
 		"@types/bcryptjs": "2.4.1",
 		"@types/body-parser": "1.16.8",
-		"@types/chai": "4.0.8",
+		"@types/chai": "4.0.10",
 		"@types/chai-http": "3.0.3",
 		"@types/compression": "0.0.35",
 		"@types/cookie": "0.3.1",

From 442ed1639a39e641c5ac2bfc7b05c56bfae25c12 Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Tue, 12 Dec 2017 01:27:35 +0000
Subject: [PATCH 066/186] fix(package): update @types/request to version 2.0.9

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index db3b08d31..b8d2a644e 100644
--- a/package.json
+++ b/package.json
@@ -65,7 +65,7 @@
 		"@types/qrcode": "0.8.0",
 		"@types/ratelimiter": "2.1.28",
 		"@types/redis": "2.8.2",
-		"@types/request": "2.0.8",
+		"@types/request": "2.0.9",
 		"@types/rimraf": "2.0.2",
 		"@types/riot": "3.6.1",
 		"@types/seedrandom": "2.4.27",

From b0bcdc929b29cca5102acd326fc055ee0fc681ce Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Tue, 12 Dec 2017 21:31:03 +0900
Subject: [PATCH 067/186] Fix

---
 src/web/app/desktop/tags/ui.tag | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/web/app/desktop/tags/ui.tag b/src/web/app/desktop/tags/ui.tag
index bc0a937a5..059d88528 100644
--- a/src/web/app/desktop/tags/ui.tag
+++ b/src/web/app/desktop/tags/ui.tag
@@ -165,7 +165,7 @@
 					transition color 0.5s ease, border 0.5s ease
 					font-family FontAwesome, sans-serif
 
-					&:placeholder-shown
+					&::placeholder
 						color #9eaba8
 
 					&:hover

From bc200034bd7c237da6dafbfaf7ed326d6794ffd1 Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Wed, 13 Dec 2017 20:13:39 +0900
Subject: [PATCH 068/186] Update config.md

---
 docs/config.md | 8 ++++++++
 1 file changed, 8 insertions(+)

diff --git a/docs/config.md b/docs/config.md
index 653fff1a7..a9987c9ce 100644
--- a/docs/config.md
+++ b/docs/config.md
@@ -49,4 +49,12 @@ sw:
   # VAPIDの秘密鍵
   private_key:
 
+# Twitterインテグレーションの設定(利用しない場合は省略可能)
+twitter:
+  # インテグレーション用アプリのコンシューマーキー
+  consumer_key: 
+
+  # インテグレーション用アプリのコンシューマーシークレット
+  consumer_secret: 
+
 ```

From 007494269888ffa2f1c311e07880e0abc43d026e Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Wed, 13 Dec 2017 19:27:08 +0000
Subject: [PATCH 069/186] fix(package): update @types/node to version 8.5.0

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index db366a4a0..c801b1e90 100644
--- a/package.json
+++ b/package.json
@@ -59,7 +59,7 @@
 		"@types/morgan": "1.7.35",
 		"@types/ms": "0.7.30",
 		"@types/multer": "1.3.6",
-		"@types/node": "8.0.58",
+		"@types/node": "8.5.0",
 		"@types/page": "1.5.32",
 		"@types/proxy-addr": "2.0.0",
 		"@types/qrcode": "0.8.0",

From 09829274f2ec8420a9cbcb8bffe801940f3c22a8 Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Wed, 13 Dec 2017 20:31:12 +0000
Subject: [PATCH 070/186] fix(package): update @types/redis to version 2.8.3

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index c801b1e90..724ff2043 100644
--- a/package.json
+++ b/package.json
@@ -64,7 +64,7 @@
 		"@types/proxy-addr": "2.0.0",
 		"@types/qrcode": "0.8.0",
 		"@types/ratelimiter": "2.1.28",
-		"@types/redis": "2.8.2",
+		"@types/redis": "2.8.3",
 		"@types/request": "2.0.9",
 		"@types/rimraf": "2.0.2",
 		"@types/riot": "3.6.1",

From 02d68cc80f467a230c450efc15ace661be755c83 Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Thu, 14 Dec 2017 07:11:20 +0900
Subject: [PATCH 071/186] Update post-form.tag

---
 src/web/app/mobile/tags/post-form.tag | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/web/app/mobile/tags/post-form.tag b/src/web/app/mobile/tags/post-form.tag
index f09f40bb5..05466a6ec 100644
--- a/src/web/app/mobile/tags/post-form.tag
+++ b/src/web/app/mobile/tags/post-form.tag
@@ -12,7 +12,7 @@
 		<div class="attaches" show={ files.length != 0 }>
 			<ul class="files" ref="attaches">
 				<li class="file" each={ files } data-id={ id }>
-					<div class="img" style="background-image: url({ url + '?thumbnail&size=64' })" title={ name } onclick={ removeFile }></div>
+					<div class="img" style="background-image: url({ url + '?thumbnail&size=128' })" onclick={ removeFile }></div>
 				</li>
 			</ul>
 		</div>

From 84dc636d79544a7566fd73a67619c19b9d07197c Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Wed, 13 Dec 2017 23:26:25 +0000
Subject: [PATCH 072/186] fix(package): update @types/inquirer to version
 0.0.36

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index 724ff2043..7725dcbce 100644
--- a/package.json
+++ b/package.json
@@ -49,7 +49,7 @@
 		"@types/gulp-replace": "0.0.31",
 		"@types/gulp-uglify": "3.0.3",
 		"@types/gulp-util": "3.0.34",
-		"@types/inquirer": "0.0.35",
+		"@types/inquirer": "0.0.36",
 		"@types/is-root": "1.0.0",
 		"@types/is-url": "1.2.28",
 		"@types/js-yaml": "3.10.1",

From 7ddadd2ab03902085072c43354bb4109864975ee Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Wed, 13 Dec 2017 23:47:49 +0000
Subject: [PATCH 073/186] fix(package): update @types/node to version 8.5.1

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index 724ff2043..bc43909a3 100644
--- a/package.json
+++ b/package.json
@@ -59,7 +59,7 @@
 		"@types/morgan": "1.7.35",
 		"@types/ms": "0.7.30",
 		"@types/multer": "1.3.6",
-		"@types/node": "8.5.0",
+		"@types/node": "8.5.1",
 		"@types/page": "1.5.32",
 		"@types/proxy-addr": "2.0.0",
 		"@types/qrcode": "0.8.0",

From 6d3bfd4880b6bb8e51e5331bbfc1651834df8ec8 Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Thu, 14 Dec 2017 02:53:58 +0000
Subject: [PATCH 074/186] fix(package): update uglifyjs-webpack-plugin to
 version 1.1.3

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index 724ff2043..84e301743 100644
--- a/package.json
+++ b/package.json
@@ -165,7 +165,7 @@
 		"tslint": "5.8.0",
 		"typescript": "2.6.2",
 		"uglify-es": "3.2.0",
-		"uglifyjs-webpack-plugin": "1.1.2",
+		"uglifyjs-webpack-plugin": "1.1.3",
 		"uuid": "3.1.0",
 		"vhost": "3.0.2",
 		"web-push": "3.2.5",

From d408d19f72e0ac930e55d8383fecc0109f8afced Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 14 Dec 2017 12:32:23 +0900
Subject: [PATCH 075/186] Refactor

---
 src/web/app/desktop/tags/image-dialog.tag | 61 ----------------------
 src/web/app/desktop/tags/images.tag       | 62 +++++++++++++++++++++++
 src/web/app/desktop/tags/index.ts         |  1 -
 3 files changed, 62 insertions(+), 62 deletions(-)
 delete mode 100644 src/web/app/desktop/tags/image-dialog.tag

diff --git a/src/web/app/desktop/tags/image-dialog.tag b/src/web/app/desktop/tags/image-dialog.tag
deleted file mode 100644
index 39d16ca13..000000000
--- a/src/web/app/desktop/tags/image-dialog.tag
+++ /dev/null
@@ -1,61 +0,0 @@
-<mk-image-dialog>
-	<div class="bg" ref="bg" onclick={ close }></div><img ref="img" src={ image.url } alt={ image.name } title={ image.name } onclick={ close }/>
-	<style>
-		:scope
-			display block
-			position fixed
-			z-index 2048
-			top 0
-			left 0
-			width 100%
-			height 100%
-			opacity 0
-
-			> .bg
-				display block
-				position fixed
-				z-index 1
-				top 0
-				left 0
-				width 100%
-				height 100%
-				background rgba(0, 0, 0, 0.7)
-
-			> img
-				position fixed
-				z-index 2
-				top 0
-				right 0
-				bottom 0
-				left 0
-				max-width 100%
-				max-height 100%
-				margin auto
-				cursor zoom-out
-
-	</style>
-	<script>
-		import anime from 'animejs';
-
-		this.image = this.opts.image;
-
-		this.on('mount', () => {
-			anime({
-				targets: this.root,
-				opacity: 1,
-				duration: 100,
-				easing: 'linear'
-			});
-		});
-
-		this.close = () => {
-			anime({
-				targets: this.root,
-				opacity: 0,
-				duration: 100,
-				easing: 'linear',
-				complete: () => this.unmount()
-			});
-		};
-	</script>
-</mk-image-dialog>
diff --git a/src/web/app/desktop/tags/images.tag b/src/web/app/desktop/tags/images.tag
index 29540747f..0cd408576 100644
--- a/src/web/app/desktop/tags/images.tag
+++ b/src/web/app/desktop/tags/images.tag
@@ -108,3 +108,65 @@
 		};
 	</script>
 </mk-images-image>
+
+<mk-image-dialog>
+	<div class="bg" ref="bg" onclick={ close }></div><img ref="img" src={ image.url } alt={ image.name } title={ image.name } onclick={ close }/>
+	<style>
+		:scope
+			display block
+			position fixed
+			z-index 2048
+			top 0
+			left 0
+			width 100%
+			height 100%
+			opacity 0
+
+			> .bg
+				display block
+				position fixed
+				z-index 1
+				top 0
+				left 0
+				width 100%
+				height 100%
+				background rgba(0, 0, 0, 0.7)
+
+			> img
+				position fixed
+				z-index 2
+				top 0
+				right 0
+				bottom 0
+				left 0
+				max-width 100%
+				max-height 100%
+				margin auto
+				cursor zoom-out
+
+	</style>
+	<script>
+		import anime from 'animejs';
+
+		this.image = this.opts.image;
+
+		this.on('mount', () => {
+			anime({
+				targets: this.root,
+				opacity: 1,
+				duration: 100,
+				easing: 'linear'
+			});
+		});
+
+		this.close = () => {
+			anime({
+				targets: this.root,
+				opacity: 0,
+				duration: 100,
+				easing: 'linear',
+				complete: () => this.unmount()
+			});
+		};
+	</script>
+</mk-image-dialog>
diff --git a/src/web/app/desktop/tags/index.ts b/src/web/app/desktop/tags/index.ts
index 30a13b584..4edda8353 100644
--- a/src/web/app/desktop/tags/index.ts
+++ b/src/web/app/desktop/tags/index.ts
@@ -77,7 +77,6 @@ require('./set-banner-suggestion.tag');
 require('./repost-form.tag');
 require('./sub-post-content.tag');
 require('./images.tag');
-require('./image-dialog.tag');
 require('./donation.tag');
 require('./users-list.tag');
 require('./user-following.tag');

From aff688d9bf7f55a6f91a9b50f2dd6809f13683a1 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 14 Dec 2017 13:31:17 +0900
Subject: [PATCH 076/186] :v:

---
 docs/api/endpoints/posts/create.yaml | 53 ++++++++++++++++++++++++++++
 src/api/endpoints/posts/create.ts    |  4 ++-
 test/api.js                          | 34 ++++++++++--------
 3 files changed, 76 insertions(+), 15 deletions(-)
 create mode 100644 docs/api/endpoints/posts/create.yaml

diff --git a/docs/api/endpoints/posts/create.yaml b/docs/api/endpoints/posts/create.yaml
new file mode 100644
index 000000000..db91775cb
--- /dev/null
+++ b/docs/api/endpoints/posts/create.yaml
@@ -0,0 +1,53 @@
+endpoint: "posts/create"
+
+desc:
+  ja: "投稿します。"
+  en: "Compose new post."
+
+params:
+  - name: "text"
+    type: "string"
+    required: true
+    desc:
+      ja: "投稿の本文"
+      en: "Text of a post"
+  - name: "media_ids"
+    type: "id(DriveFile)[]"
+    required: false
+    desc:
+      ja: "添付するメディア"
+      en: "Media you want to attach"
+  - name: "reply_id"
+    type: "id(Post)"
+    required: false
+    desc:
+      ja: "返信する投稿"
+      en: "A post you want to reply"
+  - name: "repost_id"
+    type: "id(Post)"
+    required: false
+    desc:
+      ja: "引用する投稿"
+      en: "A post you want to quote"
+  - name: "poll"
+    type: "object(poll)"
+    required: false
+    desc:
+      ja: "投票"
+      en: "A poll"
+
+paramDefs:
+  poll:
+    - name: "choices"
+      type: "string[]"
+      required: true
+      desc:
+        ja: "投票の選択肢"
+        en: "Choices of a poll"
+
+res:
+  - name: "created_post"
+    type: "entity(Post)"
+    desc:
+      ja: "作成した投稿"
+      en: "A post that created"
diff --git a/src/api/endpoints/posts/create.ts b/src/api/endpoints/posts/create.ts
index ae4959dae..7270efaf7 100644
--- a/src/api/endpoints/posts/create.ts
+++ b/src/api/endpoints/posts/create.ts
@@ -222,7 +222,9 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => {
 	const postObj = await serialize(post);
 
 	// Reponse
-	res(postObj);
+	res({
+		created_post: postObj
+	});
 
 	//#region Post processes
 
diff --git a/test/api.js b/test/api.js
index 49f1faa53..500b9adb7 100644
--- a/test/api.js
+++ b/test/api.js
@@ -224,7 +224,8 @@ describe('API', () => {
 			const res = await request('/posts/create', post, me);
 			res.should.have.status(200);
 			res.body.should.be.a('object');
-			res.body.should.have.property('text').eql(post.text);
+			res.body.should.have.property('created_post');
+			res.body.created_post.should.have.property('text').eql(post.text);
 		}));
 
 		it('ファイルを添付できる', async(async () => {
@@ -237,7 +238,8 @@ describe('API', () => {
 			}, me);
 			res.should.have.status(200);
 			res.body.should.be.a('object');
-			res.body.should.have.property('media_ids').eql([file._id.toString()]);
+			res.body.should.have.property('created_post');
+			res.body.created_post.should.have.property('media_ids').eql([file._id.toString()]);
 		}));
 
 		it('他人のファイルは添付できない', async(async () => {
@@ -283,10 +285,11 @@ describe('API', () => {
 			const res = await request('/posts/create', post, me);
 			res.should.have.status(200);
 			res.body.should.be.a('object');
-			res.body.should.have.property('text').eql(post.text);
-			res.body.should.have.property('reply_id').eql(post.reply_id);
-			res.body.should.have.property('reply');
-			res.body.reply.should.have.property('text').eql(himaPost.text);
+			res.body.should.have.property('created_post');
+			res.body.created_post.should.have.property('text').eql(post.text);
+			res.body.created_post.should.have.property('reply_id').eql(post.reply_id);
+			res.body.created_post.should.have.property('reply');
+			res.body.created_post.reply.should.have.property('text').eql(himaPost.text);
 		}));
 
 		it('repostできる', async(async () => {
@@ -303,9 +306,10 @@ describe('API', () => {
 			const res = await request('/posts/create', post, me);
 			res.should.have.status(200);
 			res.body.should.be.a('object');
-			res.body.should.have.property('repost_id').eql(post.repost_id);
-			res.body.should.have.property('repost');
-			res.body.repost.should.have.property('text').eql(himaPost.text);
+			res.body.should.have.property('created_post');
+			res.body.created_post.should.have.property('repost_id').eql(post.repost_id);
+			res.body.created_post.should.have.property('repost');
+			res.body.created_post.repost.should.have.property('text').eql(himaPost.text);
 		}));
 
 		it('引用repostできる', async(async () => {
@@ -323,10 +327,11 @@ describe('API', () => {
 			const res = await request('/posts/create', post, me);
 			res.should.have.status(200);
 			res.body.should.be.a('object');
-			res.body.should.have.property('text').eql(post.text);
-			res.body.should.have.property('repost_id').eql(post.repost_id);
-			res.body.should.have.property('repost');
-			res.body.repost.should.have.property('text').eql(himaPost.text);
+			res.body.should.have.property('created_post');
+			res.body.created_post.should.have.property('text').eql(post.text);
+			res.body.created_post.should.have.property('repost_id').eql(post.repost_id);
+			res.body.created_post.should.have.property('repost');
+			res.body.created_post.repost.should.have.property('text').eql(himaPost.text);
 		}));
 
 		it('文字数ぎりぎりで怒られない', async(async () => {
@@ -395,7 +400,8 @@ describe('API', () => {
 			}, me);
 			res.should.have.status(200);
 			res.body.should.be.a('object');
-			res.body.should.have.property('poll');
+			res.body.should.have.property('created_post');
+			res.body.created_post.should.have.property('poll');
 		}));
 
 		it('投票の選択肢が無くて怒られる', async(async () => {

From 5166fc92b64af25946b9c5a55ee05cebca0d24fa Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 14 Dec 2017 16:24:41 +0900
Subject: [PATCH 077/186] :pizza:

---
 gulpfile.ts                                   | 20 ++++-
 package.json                                  |  4 +
 src/web/app/app.styl                          | 38 +---------
 src/web/docs/api/endpoints/gulpfile.ts        | 75 +++++++++++++++++++
 .../web/docs}/api/endpoints/posts/create.yaml | 13 ++--
 src/web/docs/api/endpoints/style.styl         | 16 ++++
 src/web/docs/api/endpoints/view.pug           | 60 +++++++++++++++
 src/{ => web}/docs/api/entities/post.pug      |  0
 src/{ => web}/docs/api/entities/user.pug      |  0
 src/{ => web}/docs/api/getting-started.md     |  0
 src/{ => web}/docs/api/library.md             |  0
 src/{ => web}/docs/index.md                   |  0
 src/{ => web}/docs/link-to-twitter.md         |  0
 src/web/docs/style.styl                       | 69 +++++++++++++++++
 src/{ => web}/docs/tou.md                     |  0
 src/web/server.ts                             |  6 ++
 src/web/style.styl                            | 38 ++++++++++
 17 files changed, 295 insertions(+), 44 deletions(-)
 create mode 100644 src/web/docs/api/endpoints/gulpfile.ts
 rename {docs => src/web/docs}/api/endpoints/posts/create.yaml (87%)
 create mode 100644 src/web/docs/api/endpoints/style.styl
 create mode 100644 src/web/docs/api/endpoints/view.pug
 rename src/{ => web}/docs/api/entities/post.pug (100%)
 rename src/{ => web}/docs/api/entities/user.pug (100%)
 rename src/{ => web}/docs/api/getting-started.md (100%)
 rename src/{ => web}/docs/api/library.md (100%)
 rename src/{ => web}/docs/index.md (100%)
 rename src/{ => web}/docs/link-to-twitter.md (100%)
 create mode 100644 src/web/docs/style.styl
 rename src/{ => web}/docs/tou.md (100%)
 create mode 100644 src/web/style.styl

diff --git a/gulpfile.ts b/gulpfile.ts
index ee11a02dc..0bc18dd7c 100644
--- a/gulpfile.ts
+++ b/gulpfile.ts
@@ -13,6 +13,7 @@ import * as es from 'event-stream';
 import cssnano = require('gulp-cssnano');
 import * as uglifyComposer from 'gulp-uglify/composer';
 import pug = require('gulp-pug');
+import stylus = require('gulp-stylus');
 import * as rimraf from 'rimraf';
 import chalk from 'chalk';
 import imagemin = require('gulp-imagemin');
@@ -47,15 +48,32 @@ if (isDebug) {
 
 const constants = require('./src/const.json');
 
+require('./src/web/docs/api/endpoints/gulpfile.ts');
+
 gulp.task('build', [
 	'build:js',
 	'build:ts',
 	'build:copy',
-	'build:client'
+	'build:client',
+	'build:doc'
 ]);
 
 gulp.task('rebuild', ['clean', 'build']);
 
+gulp.task('build:doc', [
+	'doc:endpoints',
+	'doc:styles'
+]);
+
+gulp.task('doc:styles', () =>
+	gulp.src('./src/web/docs/**/*.styl')
+		.pipe(stylus())
+		.pipe(isProduction
+			? (cssnano as any)()
+			: gutil.noop())
+		.pipe(gulp.dest('./built/web/assets/docs/'))
+);
+
 gulp.task('build:js', () =>
 	gulp.src(['./src/**/*.js', '!./src/web/**/*.js'])
 		.pipe(gulp.dest('./built/'))
diff --git a/package.json b/package.json
index c20fd0c52..69090349e 100644
--- a/package.json
+++ b/package.json
@@ -53,6 +53,7 @@
 		"@types/is-root": "1.0.0",
 		"@types/is-url": "1.2.28",
 		"@types/js-yaml": "3.10.1",
+		"@types/mkdirp": "^0.5.2",
 		"@types/mocha": "2.2.44",
 		"@types/mongodb": "2.2.17",
 		"@types/monk": "1.0.6",
@@ -62,6 +63,7 @@
 		"@types/node": "8.5.1",
 		"@types/page": "1.5.32",
 		"@types/proxy-addr": "2.0.0",
+		"@types/pug": "^2.0.4",
 		"@types/qrcode": "0.8.0",
 		"@types/ratelimiter": "2.1.28",
 		"@types/redis": "2.8.3",
@@ -112,6 +114,7 @@
 		"gulp-pug": "3.3.0",
 		"gulp-rename": "1.2.2",
 		"gulp-replace": "0.6.1",
+		"gulp-stylus": "^2.6.0",
 		"gulp-tslint": "8.1.2",
 		"gulp-typescript": "3.2.3",
 		"gulp-uglify": "3.0.0",
@@ -122,6 +125,7 @@
 		"is-url": "1.2.2",
 		"js-yaml": "3.10.0",
 		"mecab-async": "0.1.2",
+		"mkdirp": "^0.5.1",
 		"mocha": "4.0.1",
 		"moji": "0.5.1",
 		"mongodb": "2.2.33",
diff --git a/src/web/app/app.styl b/src/web/app/app.styl
index de66df74d..22043b883 100644
--- a/src/web/app/app.styl
+++ b/src/web/app/app.styl
@@ -1,29 +1,4 @@
-json('../../const.json')
-
-@charset 'utf-8'
-
-$theme-color = themeColor
-$theme-color-foreground = themeColorForeground
-
-/*
-	::selection
-		background $theme-color
-		color #fff
-*/
-
-*
-	position relative
-	box-sizing border-box
-	background-clip padding-box !important
-	tap-highlight-color rgba($theme-color, 0.7)
-	-webkit-tap-highlight-color rgba($theme-color, 0.7)
-
-html, body
-	margin 0
-	padding 0
-	scroll-behavior smooth
-	text-size-adjust 100%
-	font-family sans-serif
+@import "../style"
 
 html
 	&.progress
@@ -96,17 +71,6 @@ body
 		100%
 			transform rotate(360deg)
 
-a
-	text-decoration none
-	color $theme-color
-	cursor pointer
-
-	&:hover
-		text-decoration underline
-
-	*
-		cursor pointer
-
 code
 	font-family Consolas, 'Courier New', Courier, Monaco, monospace
 
diff --git a/src/web/docs/api/endpoints/gulpfile.ts b/src/web/docs/api/endpoints/gulpfile.ts
new file mode 100644
index 000000000..a2c394470
--- /dev/null
+++ b/src/web/docs/api/endpoints/gulpfile.ts
@@ -0,0 +1,75 @@
+/**
+ * Gulp tasks
+ */
+
+import * as fs from 'fs';
+import * as path from 'path';
+import * as glob from 'glob';
+import * as gulp from 'gulp';
+import * as pug from 'pug';
+import * as yaml from 'js-yaml';
+import * as mkdirp from 'mkdirp';
+
+import config from './../../../../conf';
+
+const parseParam = param => {
+	const id = param.type.match(/^id\((.+?)\)/);
+	const object = param.type.match(/^object\((.+?)\)/);
+	const isArray = /\[\]$/.test(param.type);
+	if (id) {
+		param.kind = 'id';
+		param.type = 'string';
+		param.entity = id[1];
+		if (isArray) {
+			param.type += '[]';
+		}
+	}
+	if (object) {
+		param.kind = 'object';
+		param.type = 'object';
+		param.def = object[1];
+		if (isArray) {
+			param.type += '[]';
+		}
+	}
+
+	return param;
+};
+
+gulp.task('doc:endpoints', () => {
+	glob('./src/web/docs/api/endpoints/**/*.yaml', (globErr, files) => {
+		if (globErr) {
+			console.error(globErr);
+			return;
+		}
+		//console.log(files);
+		files.forEach(file => {
+			const ep = yaml.safeLoad(fs.readFileSync(file, 'utf-8'));
+			const vars = {
+				endpoint: ep.endpoint,
+				url: `${config.api_url}/${ep.endpoint}`,
+				desc: ep.desc,
+				params: ep.params.map(p => parseParam(p)),
+				paramDefs: Object.keys(ep.paramDefs).map(key => ({
+					name: key,
+					params: ep.paramDefs[key].map(p => parseParam(p))
+				})),
+				res: ep.res.map(p => parseParam(p))
+			};
+			pug.renderFile('./src/web/docs/api/endpoints/view.pug', vars, (renderErr, html) => {
+				if (renderErr) {
+					console.error(renderErr);
+					return;
+				}
+				const htmlPath = `./built/web/docs/api/endpoints/${ep.endpoint}.html`;
+				mkdirp(path.dirname(htmlPath), (mkdirErr) => {
+					if (mkdirErr) {
+						console.error(mkdirErr);
+						return;
+					}
+					fs.writeFileSync(htmlPath, html, 'utf-8');
+				});
+			});
+		});
+	});
+});
diff --git a/docs/api/endpoints/posts/create.yaml b/src/web/docs/api/endpoints/posts/create.yaml
similarity index 87%
rename from docs/api/endpoints/posts/create.yaml
rename to src/web/docs/api/endpoints/posts/create.yaml
index db91775cb..b6613038a 100644
--- a/docs/api/endpoints/posts/create.yaml
+++ b/src/web/docs/api/endpoints/posts/create.yaml
@@ -7,31 +7,31 @@ desc:
 params:
   - name: "text"
     type: "string"
-    required: true
+    optional: false
     desc:
       ja: "投稿の本文"
       en: "Text of a post"
   - name: "media_ids"
     type: "id(DriveFile)[]"
-    required: false
+    optional: true
     desc:
       ja: "添付するメディア"
       en: "Media you want to attach"
   - name: "reply_id"
     type: "id(Post)"
-    required: false
+    optional: true
     desc:
       ja: "返信する投稿"
       en: "A post you want to reply"
   - name: "repost_id"
     type: "id(Post)"
-    required: false
+    optional: true
     desc:
       ja: "引用する投稿"
       en: "A post you want to quote"
   - name: "poll"
     type: "object(poll)"
-    required: false
+    optional: true
     desc:
       ja: "投票"
       en: "A poll"
@@ -40,7 +40,7 @@ paramDefs:
   poll:
     - name: "choices"
       type: "string[]"
-      required: true
+      optional: false
       desc:
         ja: "投票の選択肢"
         en: "Choices of a poll"
@@ -48,6 +48,7 @@ paramDefs:
 res:
   - name: "created_post"
     type: "entity(Post)"
+    optional: false
     desc:
       ja: "作成した投稿"
       en: "A post that created"
diff --git a/src/web/docs/api/endpoints/style.styl b/src/web/docs/api/endpoints/style.styl
new file mode 100644
index 000000000..12c06fe3a
--- /dev/null
+++ b/src/web/docs/api/endpoints/style.styl
@@ -0,0 +1,16 @@
+@import "../../style"
+
+#url
+	padding 8px 12px
+	font-family Consolas, 'Courier New', Courier, Monaco, monospace
+	color #fff
+	background #222e40
+	border-radius 4px
+
+table
+	.name
+		font-weight bold
+
+	.type
+		font-family Consolas, 'Courier New', Courier, Monaco, monospace
+
diff --git a/src/web/docs/api/endpoints/view.pug b/src/web/docs/api/endpoints/view.pug
new file mode 100644
index 000000000..d9de9cb74
--- /dev/null
+++ b/src/web/docs/api/endpoints/view.pug
@@ -0,0 +1,60 @@
+doctype html
+
+mixin i18n(xs)
+	each text, lang in xs
+		span(class=`i18n ${lang}`)= text
+
+mixin table(params)
+	table
+		thead: tr
+			th Name
+			th Type
+			th Optional
+			th Description
+		tbody
+			each param in params
+				tr
+					td.name= param.name
+					td.type
+						if param.kind == 'id'
+							| #{param.type} (ID of
+							= ' '
+							a(href=`/docs/api/entities/${param.entity}`)= param.entity
+							| )
+						else if param.kind == 'object'
+							| #{param.type} (
+							a(href=`#${param.def}`)= param.def
+							| )
+						else
+							= param.type
+					td.optional= param.optional.toString()
+					td.desc: +i18n(param.desc)
+
+html
+	head
+		meta(charset="UTF-8")
+		title #{endpoint} | Misskey API
+		link(rel="stylesheet" href="/assets/docs/api/endpoints/style.css")
+
+	body
+		main
+			h1= endpoint
+
+			p#url= url
+
+			p#desc: +i18n(desc)
+
+			section
+				h2 Params
+				+table(params)
+
+				if paramDefs
+					each paramDef in paramDefs
+						section(id= paramDef.name)
+							h3= paramDef.name
+							+table(paramDef.params)
+
+			section
+				h2 Response
+				+table(res)
+
diff --git a/src/docs/api/entities/post.pug b/src/web/docs/api/entities/post.pug
similarity index 100%
rename from src/docs/api/entities/post.pug
rename to src/web/docs/api/entities/post.pug
diff --git a/src/docs/api/entities/user.pug b/src/web/docs/api/entities/user.pug
similarity index 100%
rename from src/docs/api/entities/user.pug
rename to src/web/docs/api/entities/user.pug
diff --git a/src/docs/api/getting-started.md b/src/web/docs/api/getting-started.md
similarity index 100%
rename from src/docs/api/getting-started.md
rename to src/web/docs/api/getting-started.md
diff --git a/src/docs/api/library.md b/src/web/docs/api/library.md
similarity index 100%
rename from src/docs/api/library.md
rename to src/web/docs/api/library.md
diff --git a/src/docs/index.md b/src/web/docs/index.md
similarity index 100%
rename from src/docs/index.md
rename to src/web/docs/index.md
diff --git a/src/docs/link-to-twitter.md b/src/web/docs/link-to-twitter.md
similarity index 100%
rename from src/docs/link-to-twitter.md
rename to src/web/docs/link-to-twitter.md
diff --git a/src/web/docs/style.styl b/src/web/docs/style.styl
new file mode 100644
index 000000000..9014df87f
--- /dev/null
+++ b/src/web/docs/style.styl
@@ -0,0 +1,69 @@
+@import "../style"
+
+body
+	margin 0
+	color #34495e
+
+main
+	padding 32px
+	width 100%
+	max-width 700px
+
+footer
+	padding:32px 0 0 0
+	margin 32px 0 0 0
+	border-top solid 1px #eee
+
+	.copyright
+		margin 16px 0 0 0
+		color #aaa
+
+section
+	margin 32px 0
+
+h1
+	margin 0 0 24px 0
+	padding 16px 0
+	font-size 1.5em
+	border-bottom solid 2px #eee
+
+h2
+	margin 0 0 24px 0
+	padding 0 0 16px 0
+	font-size 1.4em
+	border-bottom solid 1px #eee
+
+h3
+	margin 0
+	padding 0
+	font-size 1.25em
+
+h4
+	margin 0
+
+p
+	margin 1em 0
+	line-height 1.6em
+
+table
+	width 100%
+	border-spacing 0
+	border-collapse collapse
+
+	thead
+		font-weight bold
+		border-bottom solid 2px #eee
+
+		tr
+			th
+				text-align left
+
+	tbody
+		tr
+			border-bottom dashed 1px #eee
+
+	th, td
+		padding 8px 16px
+
+.i18n:not(.ja)
+	display none
diff --git a/src/docs/tou.md b/src/web/docs/tou.md
similarity index 100%
rename from src/docs/tou.md
rename to src/web/docs/tou.md
diff --git a/src/web/server.ts b/src/web/server.ts
index 1d3687f89..38e87754f 100644
--- a/src/web/server.ts
+++ b/src/web/server.ts
@@ -63,6 +63,12 @@ app.get('/manifest.json', (req, res) =>
  */
 app.get(/\/api:url/, require('./service/url-preview'));
 
+/**
+ * Docs
+ */
+app.get(/^\/docs\/([a-z_\-\/]+?)$/, (req, res) =>
+	res.sendFile(`${__dirname}/docs/${req.params[0]}.html`));
+
 /**
  * Routing
  */
diff --git a/src/web/style.styl b/src/web/style.styl
new file mode 100644
index 000000000..573df10d7
--- /dev/null
+++ b/src/web/style.styl
@@ -0,0 +1,38 @@
+json('../const.json')
+
+@charset 'utf-8'
+
+$theme-color = themeColor
+$theme-color-foreground = themeColorForeground
+
+/*
+	::selection
+		background $theme-color
+		color #fff
+*/
+
+*
+	position relative
+	box-sizing border-box
+	background-clip padding-box !important
+	tap-highlight-color rgba($theme-color, 0.7)
+	-webkit-tap-highlight-color rgba($theme-color, 0.7)
+
+html, body
+	margin 0
+	padding 0
+	scroll-behavior smooth
+	text-size-adjust 100%
+	font-family sans-serif
+
+a
+	text-decoration none
+	color $theme-color
+	cursor pointer
+
+	&:hover
+		text-decoration underline
+
+	*
+		cursor pointer
+

From bdaf4b01b17e6cb19637c6f3a42569d1ef19753f Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Thu, 14 Dec 2017 09:36:26 +0000
Subject: [PATCH 078/186] fix(package): update style-loader to version 0.19.1

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index 69090349e..97f290476 100644
--- a/package.json
+++ b/package.json
@@ -156,7 +156,7 @@
 		"sortablejs": "1.7.0",
 		"speakeasy": "2.0.0",
 		"string-replace-webpack-plugin": "0.1.3",
-		"style-loader": "0.19.0",
+		"style-loader": "0.19.1",
 		"stylus": "0.54.5",
 		"stylus-loader": "3.0.1",
 		"summaly": "2.0.3",

From 503eef4651259c5d53001e5fd7cb41cfa322221c Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Thu, 14 Dec 2017 11:34:10 +0000
Subject: [PATCH 079/186] fix(package): update uglifyjs-webpack-plugin to
 version 1.1.4

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index 69090349e..9305cac9f 100644
--- a/package.json
+++ b/package.json
@@ -169,7 +169,7 @@
 		"tslint": "5.8.0",
 		"typescript": "2.6.2",
 		"uglify-es": "3.2.0",
-		"uglifyjs-webpack-plugin": "1.1.3",
+		"uglifyjs-webpack-plugin": "1.1.4",
 		"uuid": "3.1.0",
 		"vhost": "3.0.2",
 		"web-push": "3.2.5",

From 5b6641003139606d54e3b8646b156d4fcd05b9d5 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 14 Dec 2017 22:36:04 +0900
Subject: [PATCH 080/186] :v:

---
 src/web/docs/api/endpoints/gulpfile.ts       | 39 +++++++++++++++-----
 src/web/docs/api/endpoints/posts/create.yaml | 21 +++++------
 src/web/docs/api/endpoints/view.pug          |  6 ++-
 3 files changed, 45 insertions(+), 21 deletions(-)

diff --git a/src/web/docs/api/endpoints/gulpfile.ts b/src/web/docs/api/endpoints/gulpfile.ts
index a2c394470..e375447c5 100644
--- a/src/web/docs/api/endpoints/gulpfile.ts
+++ b/src/web/docs/api/endpoints/gulpfile.ts
@@ -14,7 +14,8 @@ import config from './../../../../conf';
 
 const parseParam = param => {
 	const id = param.type.match(/^id\((.+?)\)/);
-	const object = param.type.match(/^object\((.+?)\)/);
+	const entity = param.type.match(/^entity\((.+?)\)/);
+	const isObject = /^object/.test(param.type);
 	const isArray = /\[\]$/.test(param.type);
 	if (id) {
 		param.kind = 'id';
@@ -24,18 +25,40 @@ const parseParam = param => {
 			param.type += '[]';
 		}
 	}
-	if (object) {
-		param.kind = 'object';
+	if (entity) {
+		param.kind = 'entity';
 		param.type = 'object';
-		param.def = object[1];
+		param.entity = entity[1];
 		if (isArray) {
 			param.type += '[]';
 		}
 	}
+	if (isObject) {
+		param.kind = 'object';
+	}
 
 	return param;
 };
 
+const extractDefs = params => {
+	const defs = [];
+
+	params.forEach(param => {
+		if (param.def) {
+			defs.push({
+				name: param.defName,
+				params: param.def.map(p => parseParam(p))
+			});
+
+			const childDefs = extractDefs(param.def);
+
+			defs.concat(childDefs);
+		}
+	});
+
+	return defs;
+};
+
 gulp.task('doc:endpoints', () => {
 	glob('./src/web/docs/api/endpoints/**/*.yaml', (globErr, files) => {
 		if (globErr) {
@@ -50,11 +73,9 @@ gulp.task('doc:endpoints', () => {
 				url: `${config.api_url}/${ep.endpoint}`,
 				desc: ep.desc,
 				params: ep.params.map(p => parseParam(p)),
-				paramDefs: Object.keys(ep.paramDefs).map(key => ({
-					name: key,
-					params: ep.paramDefs[key].map(p => parseParam(p))
-				})),
-				res: ep.res.map(p => parseParam(p))
+				paramDefs: extractDefs(ep.params),
+				res: ep.res.map(p => parseParam(p)),
+				resDefs: extractDefs(ep.res)
 			};
 			pug.renderFile('./src/web/docs/api/endpoints/view.pug', vars, (renderErr, html) => {
 				if (renderErr) {
diff --git a/src/web/docs/api/endpoints/posts/create.yaml b/src/web/docs/api/endpoints/posts/create.yaml
index b6613038a..feedf4f0d 100644
--- a/src/web/docs/api/endpoints/posts/create.yaml
+++ b/src/web/docs/api/endpoints/posts/create.yaml
@@ -7,7 +7,7 @@ desc:
 params:
   - name: "text"
     type: "string"
-    optional: false
+    optional: true
     desc:
       ja: "投稿の本文"
       en: "Text of a post"
@@ -30,20 +30,19 @@ params:
       ja: "引用する投稿"
       en: "A post you want to quote"
   - name: "poll"
-    type: "object(poll)"
+    type: "object"
     optional: true
     desc:
       ja: "投票"
       en: "A poll"
-
-paramDefs:
-  poll:
-    - name: "choices"
-      type: "string[]"
-      optional: false
-      desc:
-        ja: "投票の選択肢"
-        en: "Choices of a poll"
+    defName: "poll"
+    def:
+      - name: "choices"
+        type: "string[]"
+        optional: false
+        desc:
+          ja: "投票の選択肢"
+          en: "Choices of a poll"
 
 res:
   - name: "created_post"
diff --git a/src/web/docs/api/endpoints/view.pug b/src/web/docs/api/endpoints/view.pug
index d9de9cb74..b7b2658a3 100644
--- a/src/web/docs/api/endpoints/view.pug
+++ b/src/web/docs/api/endpoints/view.pug
@@ -21,9 +21,13 @@ mixin table(params)
 							= ' '
 							a(href=`/docs/api/entities/${param.entity}`)= param.entity
 							| )
+						else if param.kind == 'entity'
+							| #{param.type} (
+							a(href=`/docs/api/entities/${param.entity}`)= param.entity
+							| )
 						else if param.kind == 'object'
 							| #{param.type} (
-							a(href=`#${param.def}`)= param.def
+							a(href=`#${param.defName}`)= param.defName
 							| )
 						else
 							= param.type

From a357d5c6a51b1d3b36e2735068a2603ff8e1675a Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 14 Dec 2017 22:50:41 +0900
Subject: [PATCH 081/186] :v:

---
 src/web/docs/api/endpoints/posts/create.yaml | 4 ++--
 src/web/docs/api/endpoints/style.styl        | 2 ++
 src/web/docs/api/endpoints/view.pug          | 5 ++---
 src/web/docs/style.styl                      | 3 +++
 4 files changed, 9 insertions(+), 5 deletions(-)

diff --git a/src/web/docs/api/endpoints/posts/create.yaml b/src/web/docs/api/endpoints/posts/create.yaml
index feedf4f0d..498a99159 100644
--- a/src/web/docs/api/endpoints/posts/create.yaml
+++ b/src/web/docs/api/endpoints/posts/create.yaml
@@ -15,8 +15,8 @@ params:
     type: "id(DriveFile)[]"
     optional: true
     desc:
-      ja: "添付するメディア"
-      en: "Media you want to attach"
+      ja: "添付するメディア(1~4つ)"
+      en: "Media you want to attach (1~4)"
   - name: "reply_id"
     type: "id(Post)"
     optional: true
diff --git a/src/web/docs/api/endpoints/style.styl b/src/web/docs/api/endpoints/style.styl
index 12c06fe3a..ab74e100b 100644
--- a/src/web/docs/api/endpoints/style.styl
+++ b/src/web/docs/api/endpoints/style.styl
@@ -11,6 +11,8 @@ table
 	.name
 		font-weight bold
 
+	.name
 	.type
+	.optional
 		font-family Consolas, 'Courier New', Courier, Monaco, monospace
 
diff --git a/src/web/docs/api/endpoints/view.pug b/src/web/docs/api/endpoints/view.pug
index b7b2658a3..841ca8b3f 100644
--- a/src/web/docs/api/endpoints/view.pug
+++ b/src/web/docs/api/endpoints/view.pug
@@ -17,10 +17,9 @@ mixin table(params)
 					td.name= param.name
 					td.type
 						if param.kind == 'id'
-							| #{param.type} (ID of
-							= ' '
+							| #{param.type} (
 							a(href=`/docs/api/entities/${param.entity}`)= param.entity
-							| )
+							|  ID)
 						else if param.kind == 'entity'
 							| #{param.type} (
 							a(href=`/docs/api/entities/${param.entity}`)= param.entity
diff --git a/src/web/docs/style.styl b/src/web/docs/style.styl
index 9014df87f..5c484adc1 100644
--- a/src/web/docs/style.styl
+++ b/src/web/docs/style.styl
@@ -62,6 +62,9 @@ table
 		tr
 			border-bottom dashed 1px #eee
 
+			&:nth-child(odd)
+				background #fbfbfb
+
 	th, td
 		padding 8px 16px
 

From d6ec5f2fe13bb1e3f4316f04591bf419f587c2bd Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 15 Dec 2017 00:23:45 +0900
Subject: [PATCH 082/186] :v:

---
 gulpfile.ts                                  |   4 +-
 src/web/docs/api/endpoints/posts/create.yaml |   8 +-
 src/web/docs/api/endpoints/style.styl        |  12 +-
 src/web/docs/api/endpoints/view.pug          |  75 ++++-------
 src/web/docs/api/entities/post.yaml          | 124 +++++++++++++++++++
 src/web/docs/api/entities/style.styl         |   1 +
 src/web/docs/api/entities/view.pug           |  23 ++++
 src/web/docs/api/{endpoints => }/gulpfile.ts |  78 ++++++++++--
 src/web/docs/api/mixins.pug                  |  33 +++++
 src/web/docs/api/style.styl                  |  11 ++
 src/web/docs/layout.pug                      |  16 +++
 11 files changed, 305 insertions(+), 80 deletions(-)
 create mode 100644 src/web/docs/api/entities/post.yaml
 create mode 100644 src/web/docs/api/entities/style.styl
 create mode 100644 src/web/docs/api/entities/view.pug
 rename src/web/docs/api/{endpoints => }/gulpfile.ts (50%)
 create mode 100644 src/web/docs/api/mixins.pug
 create mode 100644 src/web/docs/api/style.styl
 create mode 100644 src/web/docs/layout.pug

diff --git a/gulpfile.ts b/gulpfile.ts
index 0bc18dd7c..6807b6d57 100644
--- a/gulpfile.ts
+++ b/gulpfile.ts
@@ -48,7 +48,7 @@ if (isDebug) {
 
 const constants = require('./src/const.json');
 
-require('./src/web/docs/api/endpoints/gulpfile.ts');
+require('./src/web/docs/api/gulpfile.ts');
 
 gulp.task('build', [
 	'build:js',
@@ -61,7 +61,7 @@ gulp.task('build', [
 gulp.task('rebuild', ['clean', 'build']);
 
 gulp.task('build:doc', [
-	'doc:endpoints',
+	'doc:api',
 	'doc:styles'
 ]);
 
diff --git a/src/web/docs/api/endpoints/posts/create.yaml b/src/web/docs/api/endpoints/posts/create.yaml
index 498a99159..5e2307dab 100644
--- a/src/web/docs/api/endpoints/posts/create.yaml
+++ b/src/web/docs/api/endpoints/posts/create.yaml
@@ -10,7 +10,7 @@ params:
     optional: true
     desc:
       ja: "投稿の本文"
-      en: "Text of a post"
+      en: "The text of your post"
   - name: "media_ids"
     type: "id(DriveFile)[]"
     optional: true
@@ -22,19 +22,19 @@ params:
     optional: true
     desc:
       ja: "返信する投稿"
-      en: "A post you want to reply"
+      en: "The post you want to reply"
   - name: "repost_id"
     type: "id(Post)"
     optional: true
     desc:
       ja: "引用する投稿"
-      en: "A post you want to quote"
+      en: "The post you want to quote"
   - name: "poll"
     type: "object"
     optional: true
     desc:
       ja: "投票"
-      en: "A poll"
+      en: "The poll"
     defName: "poll"
     def:
       - name: "choices"
diff --git a/src/web/docs/api/endpoints/style.styl b/src/web/docs/api/endpoints/style.styl
index ab74e100b..07fb7ec2a 100644
--- a/src/web/docs/api/endpoints/style.styl
+++ b/src/web/docs/api/endpoints/style.styl
@@ -1,4 +1,4 @@
-@import "../../style"
+@import "../style"
 
 #url
 	padding 8px 12px
@@ -6,13 +6,3 @@
 	color #fff
 	background #222e40
 	border-radius 4px
-
-table
-	.name
-		font-weight bold
-
-	.name
-	.type
-	.optional
-		font-family Consolas, 'Courier New', Courier, Monaco, monospace
-
diff --git a/src/web/docs/api/endpoints/view.pug b/src/web/docs/api/endpoints/view.pug
index 841ca8b3f..cebef9fa5 100644
--- a/src/web/docs/api/endpoints/view.pug
+++ b/src/web/docs/api/endpoints/view.pug
@@ -1,63 +1,30 @@
-doctype html
+extends ../../layout.pug
+include ../mixins
 
-mixin i18n(xs)
-	each text, lang in xs
-		span(class=`i18n ${lang}`)= text
+block title
+	| #{endpoint} | Misskey API
 
-mixin table(params)
-	table
-		thead: tr
-			th Name
-			th Type
-			th Optional
-			th Description
-		tbody
-			each param in params
-				tr
-					td.name= param.name
-					td.type
-						if param.kind == 'id'
-							| #{param.type} (
-							a(href=`/docs/api/entities/${param.entity}`)= param.entity
-							|  ID)
-						else if param.kind == 'entity'
-							| #{param.type} (
-							a(href=`/docs/api/entities/${param.entity}`)= param.entity
-							| )
-						else if param.kind == 'object'
-							| #{param.type} (
-							a(href=`#${param.defName}`)= param.defName
-							| )
-						else
-							= param.type
-					td.optional= param.optional.toString()
-					td.desc: +i18n(param.desc)
+block meta
+	link(rel="stylesheet" href="/assets/docs/api/endpoints/style.css")
 
-html
-	head
-		meta(charset="UTF-8")
-		title #{endpoint} | Misskey API
-		link(rel="stylesheet" href="/assets/docs/api/endpoints/style.css")
+block main
+	h1= endpoint
 
-	body
-		main
-			h1= endpoint
+	p#url= url
 
-			p#url= url
+	p#desc: +i18n(desc)
 
-			p#desc: +i18n(desc)
+	section
+		h2 Params
+		+propTable(params)
 
-			section
-				h2 Params
-				+table(params)
+		if paramDefs
+			each paramDef in paramDefs
+				section(id= paramDef.name)
+					h3= paramDef.name
+					+propTable(paramDef.params)
 
-				if paramDefs
-					each paramDef in paramDefs
-						section(id= paramDef.name)
-							h3= paramDef.name
-							+table(paramDef.params)
-
-			section
-				h2 Response
-				+table(res)
+	section
+		h2 Response
+		+propTable(res)
 
diff --git a/src/web/docs/api/entities/post.yaml b/src/web/docs/api/entities/post.yaml
new file mode 100644
index 000000000..551f3b7c3
--- /dev/null
+++ b/src/web/docs/api/entities/post.yaml
@@ -0,0 +1,124 @@
+name: "Post"
+
+desc:
+  ja: "投稿。"
+  en: "A post."
+
+props:
+  - name: "id"
+    type: "id"
+    optional: false
+    desc:
+      ja: "投稿ID"
+      en: "The ID of this post"
+  - name: "created_at"
+    type: "date"
+    optional: false
+    desc:
+      ja: "投稿日時"
+      en: "The posted date of this post"
+  - name: "text"
+    type: "string"
+    optional: true
+    desc:
+      ja: "投稿の本文"
+      en: "The text of this post"
+  - name: "media_ids"
+    type: "id(DriveFile)[]"
+    optional: true
+    desc:
+      ja: "添付されているメディアのID"
+      en: "The IDs of the attached media"
+  - name: "media"
+    type: "entity(DriveFile)[]"
+    optional: true
+    desc:
+      ja: "添付されているメディア"
+      en: "The attached media"
+  - name: "user_id"
+    type: "id(User)"
+    optional: false
+    desc:
+      ja: "投稿者ID"
+      en: "The ID of author of this post"
+  - name: "user"
+    type: "entity(User)"
+    optional: true
+    desc:
+      ja: "投稿者"
+      en: "The author of this post"
+  - name: "my_reaction"
+    type: "string"
+    optional: true
+    desc:
+      ja: "この投稿に対する自分の<a href='/docs/api/reactions'>リアクション</a>"
+      en: "The your <a href='/docs/api/reactions'>reaction</a> of this post"
+  - name: "reaction_counts"
+    type: "object"
+    optional: false
+    desc:
+      ja: "<a href='/docs/api/reactions'>リアクション</a>をキーとし、この投稿に対するそのリアクションの数を値としたオブジェクト"
+  - name: "reply_id"
+    type: "id(Post)"
+    optional: true
+    desc:
+      ja: "返信した投稿のID"
+      en: "The ID of the replyed post"
+  - name: "reply"
+    type: "entity(Post)"
+    optional: true
+    desc:
+      ja: "返信した投稿"
+      en: "The replyed post"
+  - name: "repost_id"
+    type: "id(Post)"
+    optional: true
+    desc:
+      ja: "引用した投稿のID"
+      en: "The ID of the quoted post"
+  - name: "repost"
+    type: "entity(Post)"
+    optional: true
+    desc:
+      ja: "引用した投稿"
+      en: "The quoted post"
+  - name: "poll"
+    type: "object"
+    optional: true
+    desc:
+      ja: "投票"
+      en: "The poll"
+    defName: "poll"
+    def:
+      - name: "choices"
+        type: "object[]"
+        optional: false
+        desc:
+          ja: "投票の選択肢"
+          en: "The choices of this poll"
+        defName: "choice"
+        def:
+          - name: "id"
+            type: "number"
+            optional: false
+            desc:
+              ja: "選択肢ID"
+              en: "The ID of this choice"
+          - name: "is_voted"
+            type: "boolean"
+            optional: true
+            desc:
+              ja: "自分がこの選択肢に投票したかどうか"
+              en: "Whether you voted to this choice"
+          - name: "text"
+            type: "string"
+            optional: false
+            desc:
+              ja: "選択肢本文"
+              en: "The text of this choice"
+          - name: "votes"
+            type: "number"
+            optional: false
+            desc:
+              ja: "この選択肢に投票された数"
+              en: "The number voted for this choice"
diff --git a/src/web/docs/api/entities/style.styl b/src/web/docs/api/entities/style.styl
new file mode 100644
index 000000000..bddf0f53a
--- /dev/null
+++ b/src/web/docs/api/entities/style.styl
@@ -0,0 +1 @@
+@import "../style"
diff --git a/src/web/docs/api/entities/view.pug b/src/web/docs/api/entities/view.pug
new file mode 100644
index 000000000..f210582f1
--- /dev/null
+++ b/src/web/docs/api/entities/view.pug
@@ -0,0 +1,23 @@
+extends ../../layout.pug
+include ../mixins
+
+block title
+	| #{name} | Misskey API
+
+block meta
+	link(rel="stylesheet" href="/assets/docs/api/entities/style.css")
+
+block main
+	h1= name
+
+	p#desc: +i18n(desc)
+
+	section
+		h2 Properties
+		+propTable(props)
+
+		if propDefs
+			each propDef in propDefs
+				section(id= propDef.name)
+					h3= propDef.name
+					+propTable(propDef.params)
diff --git a/src/web/docs/api/endpoints/gulpfile.ts b/src/web/docs/api/gulpfile.ts
similarity index 50%
rename from src/web/docs/api/endpoints/gulpfile.ts
rename to src/web/docs/api/gulpfile.ts
index e375447c5..05567b623 100644
--- a/src/web/docs/api/endpoints/gulpfile.ts
+++ b/src/web/docs/api/gulpfile.ts
@@ -10,12 +10,15 @@ import * as pug from 'pug';
 import * as yaml from 'js-yaml';
 import * as mkdirp from 'mkdirp';
 
-import config from './../../../../conf';
+import config from './../../../conf';
+
+const kebab = string => string.replace(/([a-z])([A-Z])/g, '$1-$2').replace(/\s+/g, '-').toLowerCase();
 
 const parseParam = param => {
-	const id = param.type.match(/^id\((.+?)\)/);
+	const id = param.type.match(/^id\((.+?)\)|^id/);
 	const entity = param.type.match(/^entity\((.+?)\)/);
 	const isObject = /^object/.test(param.type);
+	const isDate = /^date/.test(param.type);
 	const isArray = /\[\]$/.test(param.type);
 	if (id) {
 		param.kind = 'id';
@@ -36,30 +39,53 @@ const parseParam = param => {
 	if (isObject) {
 		param.kind = 'object';
 	}
+	if (isDate) {
+		param.kind = 'date';
+		param.type = 'string';
+		if (isArray) {
+			param.type += '[]';
+		}
+	}
 
 	return param;
 };
 
+const sortParams = params => {
+	params.sort((a, b) => {
+		if (a.name < b.name)
+			return -1;
+		if (a.name > b.name)
+			return 1;
+		return 0;
+	});
+	return params;
+};
+
 const extractDefs = params => {
-	const defs = [];
+	let defs = [];
 
 	params.forEach(param => {
 		if (param.def) {
 			defs.push({
 				name: param.defName,
-				params: param.def.map(p => parseParam(p))
+				params: sortParams(param.def.map(p => parseParam(p)))
 			});
 
 			const childDefs = extractDefs(param.def);
 
-			defs.concat(childDefs);
+			defs = defs.concat(childDefs);
 		}
 	});
 
 	return defs;
 };
 
-gulp.task('doc:endpoints', () => {
+gulp.task('doc:api', [
+	'doc:api:endpoints',
+	'doc:api:entities'
+]);
+
+gulp.task('doc:api:endpoints', () => {
 	glob('./src/web/docs/api/endpoints/**/*.yaml', (globErr, files) => {
 		if (globErr) {
 			console.error(globErr);
@@ -72,10 +98,11 @@ gulp.task('doc:endpoints', () => {
 				endpoint: ep.endpoint,
 				url: `${config.api_url}/${ep.endpoint}`,
 				desc: ep.desc,
-				params: ep.params.map(p => parseParam(p)),
+				params: sortParams(ep.params.map(p => parseParam(p))),
 				paramDefs: extractDefs(ep.params),
-				res: ep.res.map(p => parseParam(p)),
-				resDefs: extractDefs(ep.res)
+				res: sortParams(ep.res.map(p => parseParam(p))),
+				resDefs: extractDefs(ep.res),
+				kebab
 			};
 			pug.renderFile('./src/web/docs/api/endpoints/view.pug', vars, (renderErr, html) => {
 				if (renderErr) {
@@ -94,3 +121,36 @@ gulp.task('doc:endpoints', () => {
 		});
 	});
 });
+
+gulp.task('doc:api:entities', () => {
+	glob('./src/web/docs/api/entities/**/*.yaml', (globErr, files) => {
+		if (globErr) {
+			console.error(globErr);
+			return;
+		}
+		files.forEach(file => {
+			const entity = yaml.safeLoad(fs.readFileSync(file, 'utf-8'));
+			const vars = {
+				name: entity.name,
+				desc: entity.desc,
+				props: sortParams(entity.props.map(p => parseParam(p))),
+				propDefs: extractDefs(entity.props),
+				kebab
+			};
+			pug.renderFile('./src/web/docs/api/entities/view.pug', vars, (renderErr, html) => {
+				if (renderErr) {
+					console.error(renderErr);
+					return;
+				}
+				const htmlPath = `./built/web/docs/api/entities/${kebab(entity.name)}.html`;
+				mkdirp(path.dirname(htmlPath), (mkdirErr) => {
+					if (mkdirErr) {
+						console.error(mkdirErr);
+						return;
+					}
+					fs.writeFileSync(htmlPath, html, 'utf-8');
+				});
+			});
+		});
+	});
+});
diff --git a/src/web/docs/api/mixins.pug b/src/web/docs/api/mixins.pug
new file mode 100644
index 000000000..b302c7826
--- /dev/null
+++ b/src/web/docs/api/mixins.pug
@@ -0,0 +1,33 @@
+mixin propTable(props)
+	table.props
+		thead: tr
+			th Name
+			th Type
+			th Optional
+			th Description
+		tbody
+			each prop in props
+				tr
+					td.name= prop.name
+					td.type
+						i= prop.type
+						if prop.kind == 'id'
+							if prop.entity
+								|  (
+								a(href=`/docs/api/entities/${kebab(prop.entity)}`)= prop.entity
+								|  ID)
+							else
+								|  (ID)
+						else if prop.kind == 'entity'
+							|   (
+							a(href=`/docs/api/entities/${kebab(prop.entity)}`)= prop.entity
+							| )
+						else if prop.kind == 'object'
+							if prop.def
+								|  (
+								a(href=`#${prop.defName}`)= prop.defName
+								| )
+						else if prop.kind == 'date'
+							|  (Date)
+					td.optional= prop.optional.toString()
+					td.desc: +i18n(prop.desc)
diff --git a/src/web/docs/api/style.styl b/src/web/docs/api/style.styl
new file mode 100644
index 000000000..3675a4da6
--- /dev/null
+++ b/src/web/docs/api/style.styl
@@ -0,0 +1,11 @@
+@import "../style"
+
+table.props
+	.name
+		font-weight bold
+
+	.name
+	.type
+	.optional
+		font-family Consolas, 'Courier New', Courier, Monaco, monospace
+
diff --git a/src/web/docs/layout.pug b/src/web/docs/layout.pug
new file mode 100644
index 000000000..68ca9eb62
--- /dev/null
+++ b/src/web/docs/layout.pug
@@ -0,0 +1,16 @@
+doctype html
+
+mixin i18n(xs)
+	each text, lang in xs
+		span(class=`i18n ${lang}`)!= text
+
+html
+	head
+		meta(charset="UTF-8")
+		title
+			block title
+		block meta
+
+	body
+		main
+			block main

From 13e4034ceee1e3983c852a2c40ce89eeb30dcecd Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 15 Dec 2017 00:54:28 +0900
Subject: [PATCH 083/186] :v:

---
 src/web/docs/api/entities/post.pug  | 149 ----------------------------
 src/web/docs/api/entities/user.yaml | 137 +++++++++++++++++++++++++
 src/web/docs/api/gulpfile.ts        |   2 +-
 3 files changed, 138 insertions(+), 150 deletions(-)
 delete mode 100644 src/web/docs/api/entities/post.pug
 create mode 100644 src/web/docs/api/entities/user.yaml

diff --git a/src/web/docs/api/entities/post.pug b/src/web/docs/api/entities/post.pug
deleted file mode 100644
index 954f17271..000000000
--- a/src/web/docs/api/entities/post.pug
+++ /dev/null
@@ -1,149 +0,0 @@
-extend ../../BASE
-
-block title
-	| Entity: Post
-
-block content
-	h1 Post
-	p 投稿を表します。
-
-	section
-		h2 Properties
-		table.entity
-			thead: tr
-				td Name
-				td Type
-				td Description
-			tbody
-				tr.nullable.optional
-					td app
-					td: a(href='./app', target='_blank') App
-					td 投稿したアプリ
-				tr.nullable
-					td app_id
-					td ID
-					td 投稿したアプリのID
-				tr
-					td created_at
-					td Date
-					td 投稿日時
-				tr
-					td id
-					td ID
-					td 投稿ID
-				tr.optional
-					td is_liked
-					td Boolean
-					td いいね したかどうか
-				tr
-					td likes_count
-					td Number
-					td いいね数
-				tr.nullable.optional
-					td media_ids
-					td ID[]
-					td 添付されたメディアのIDの配列
-				tr.nullable.optional
-					td media
-					td: a(href='./drive-file', target='_blank') DriveFile[]
-					td 添付されたメディアの配列
-				tr
-					td replies_count
-					td Number
-					td 返信数
-				tr.optional
-					td reply
-					td: a(href='./post', target='_blank') Post
-					td 返信先の投稿
-				tr.nullable
-					td reply_id
-					td ID
-					td 返信先の投稿のID
-				tr.optional
-					td repost
-					td: a(href='./post', target='_blank') Post
-					td Repostした投稿
-				tr
-					td repost_count
-					td Number
-					td Repostされた数
-				tr.nullable
-					td repost_id
-					td ID
-					td Repostした投稿のID
-				tr.nullable
-					td text
-					td String
-					td 本文
-				tr.optional
-					td user
-					td: a(href='./user', target='_blank') User
-					td 投稿者
-				tr
-					td user_id
-					td ID
-					td 投稿者のID
-
-	section
-		h2 Example
-		pre: code.
-			{
-				"created_at": "2016-12-10T00:28:50.114Z",
-				"media_ids": null,
-				"reply_id": "584a16b15860fc52320137e3",
-				"repost_id": null,
-				"text": "小日向美穂だぞ!",
-				"user_id": "5848bf7764e572683f4402f8",
-				"app_id": null,
-				"likes_count": 1,
-				"replies_count": 1,
-				"id": "584b4c42d8e5186f8f755d0c",
-				"user": {
-					"birthday": null,
-					"created_at": "2016-12-08T02:03:35.332Z",
-					"bio": "女が嫌いです、女性は好きです",
-					"followers_count": 11,
-					"following_count": 11,
-					"links": null,
-					"location": "",
-					"name": "女が嫌い",
-					"posts_count": 26,
-					"likes_count": 2,
-					"liked_count": 20,
-					"username": "onnnagakirai",
-					"id": "5848bf7764e572683f4402f8",
-					"avatar_url": "https://file.himasaku.net/5848c0ec64e572683f4402fc",
-					"banner_url": "https://file.himasaku.net/5848c12864e572683f4402fd",
-					"is_following": true,
-					"is_followed": true
-				},
-				"reply": {
-					"created_at": "2016-12-09T02:28:01.563Z",
-					"media_ids": null,
-					"reply_id": "5849d35e547e4249be329884",
-					"repost_id": null,
-					"text": "アイコン小日向美穂?",
-					"user_id": "57d01a501fdf2d07be417afe",
-					"app_id": null,
-					"replies_count": 1,
-					"id": "584a16b15860fc52320137e3",
-					"user": {
-						"birthday": null,
-						"created_at": "2016-09-07T13:46:56.605Z",
-						"bio": "どうすれば君だけのために生きていけるの",
-						"followers_count": 51,
-						"following_count": 97,
-						"links": null,
-						"location": "川崎",
-						"name": "きな子",
-						"posts_count": 4813,
-						"username": "syuilo",
-						"likes_count": 3141,
-						"liked_count": 750,
-						"id": "57d01a501fdf2d07be417afe",
-						"avatar_url": "https://file.himasaku.net/583ddc6e64df272771f74c1a",
-						"banner_url": "https://file.himasaku.net/584bfc82d8e5186f8f755ec5"
-					}
-				},
-				"is_liked": true
-			}
diff --git a/src/web/docs/api/entities/user.yaml b/src/web/docs/api/entities/user.yaml
new file mode 100644
index 000000000..9b1efd1fe
--- /dev/null
+++ b/src/web/docs/api/entities/user.yaml
@@ -0,0 +1,137 @@
+name: "User"
+
+desc:
+  ja: "ユーザー。"
+  en: "A user."
+
+props:
+  - name: "id"
+    type: "id"
+    optional: false
+    desc:
+      ja: "ユーザーID"
+      en: "The ID of this user"
+  - name: "created_at"
+    type: "date"
+    optional: false
+    desc:
+      ja: "アカウント作成日時"
+      en: "The registered date of this user"
+  - name: "username"
+    type: "string"
+    optional: false
+    desc:
+      ja: "ユーザー名"
+      en: "The username of this user"
+  - name: "description"
+    type: "string"
+    optional: false
+    desc:
+      ja: "アカウントの説明(自己紹介)"
+      en: "The description of this user"
+  - name: "avatar_id"
+    type: "id(DriveFile)"
+    optional: true
+    desc:
+      ja: "アバターのID"
+      en: "The ID of the avatar of this user"
+  - name: "avatar_url"
+    type: "string"
+    optional: false
+    desc:
+      ja: "アバターのURL"
+      en: "The URL of the avatar of this user"
+  - name: "banner_id"
+    type: "id(DriveFile)"
+    optional: true
+    desc:
+      ja: "バナーのID"
+      en: "The ID of the banner of this user"
+  - name: "banner_url"
+    type: "string"
+    optional: false
+    desc:
+      ja: "バナーのURL"
+      en: "The URL of the banner of this user"
+  - name: "followers_count"
+    type: "number"
+    optional: false
+    desc:
+      ja: "フォロワーの数"
+      en: "The number of the followers for this user"
+  - name: "following_count"
+    type: "number"
+    optional: false
+    desc:
+      ja: "フォローしているユーザーの数"
+      en: "The number of the following users for this user"
+  - name: "last_used_at"
+    type: "date"
+    optional: false
+    desc:
+      ja: "最終利用日時"
+      en: "The last used date of this user"
+  - name: "posts_count"
+    type: "number"
+    optional: false
+    desc:
+      ja: "投稿の数"
+      en: "The number of the posts of this user"
+  - name: "pinned_post"
+    type: "entity(Post)"
+    optional: true
+    desc:
+      ja: "ピン留めされた投稿"
+      en: "The pinned post of this user"
+  - name: "pinned_post_id"
+    type: "id(Post)"
+    optional: true
+    desc:
+      ja: "ピン留めされた投稿のID"
+      en: "The ID of the pinned post of this user"
+  - name: "drive_capacity"
+    type: "number"
+    optional: false
+    desc:
+      ja: "ドライブの容量(bytes)"
+      en: "The capacity of drive of this user (bytes)"
+  - name: "twitter"
+    type: "object"
+    optional: true
+    desc:
+      ja: "連携されているTwitterアカウント情報"
+      en: "The info of the connected twitter account of this user"
+    defName: "twitter"
+    def:
+      - name: "user_id"
+        type: "string"
+        optional: false
+        desc:
+          ja: "ユーザーID"
+          en: "The user ID"
+      - name: "screen_name"
+        type: "string"
+        optional: false
+        desc:
+          ja: "ユーザー名"
+          en: "The screen name of this user"
+  - name: "profile"
+    type: "object"
+    optional: false
+    desc:
+      ja: "プロフィール"
+      en: "The profile of this user"
+    defName: "profile"
+    def:
+      - name: "location"
+        type: "string"
+        optional: true
+        desc:
+          ja: "場所"
+          en: "The location of this user"
+      - name: "birthday"
+        type: "string"
+        optional: true
+        desc:
+          ja: "誕生日 (YYYY-MM-DD)"
+          en: "The birthday of this user (YYYY-MM-DD)"
diff --git a/src/web/docs/api/gulpfile.ts b/src/web/docs/api/gulpfile.ts
index 05567b623..6453996d3 100644
--- a/src/web/docs/api/gulpfile.ts
+++ b/src/web/docs/api/gulpfile.ts
@@ -77,7 +77,7 @@ const extractDefs = params => {
 		}
 	});
 
-	return defs;
+	return sortParams(defs);
 };
 
 gulp.task('doc:api', [

From 55e266fbbe55ea1dc8bd5cdce4e52d5625eea38d Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 15 Dec 2017 04:43:48 +0900
Subject: [PATCH 084/186] :art:

---
 src/web/docs/style.styl | 5 +++++
 1 file changed, 5 insertions(+)

diff --git a/src/web/docs/style.styl b/src/web/docs/style.styl
index 5c484adc1..a4abc5a9a 100644
--- a/src/web/docs/style.styl
+++ b/src/web/docs/style.styl
@@ -56,7 +56,12 @@ table
 
 		tr
 			th
+				position sticky
+				top 0
+				z-index 1
 				text-align left
+				box-shadow 0 1px 0 0 #eee
+				background #fff
 
 	tbody
 		tr

From 5725e39a707af39c0a5118a04282bbaf186ee922 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 15 Dec 2017 05:07:21 +0900
Subject: [PATCH 085/186] :v:

---
 src/web/docs/api/entities/user.pug  | 122 ----------------------------
 src/web/docs/api/entities/user.yaml |  10 +++
 2 files changed, 10 insertions(+), 122 deletions(-)
 delete mode 100644 src/web/docs/api/entities/user.pug

diff --git a/src/web/docs/api/entities/user.pug b/src/web/docs/api/entities/user.pug
deleted file mode 100644
index a37886bb1..000000000
--- a/src/web/docs/api/entities/user.pug
+++ /dev/null
@@ -1,122 +0,0 @@
-extend ../../BASE
-
-block title
-	| Entity: User
-
-block content
-	h1 User
-	p ユーザーを表します。
-
-	section
-		h2 Properties
-		table.entity
-			thead: tr
-				td Name
-				td Type
-				td Description
-			tbody
-				tr.nullable.optional
-					td avatar_id
-					td ID
-					td アバターに設定しているドライブのファイルのID
-				tr.nullable
-					td avatar_url
-					td String
-					td アバターURL
-				tr.nullable.optional
-					td banner_id
-					td ID
-					td バナーに設定しているドライブのファイルのID
-				tr.nullable
-					td banner_url
-					td String
-					td バナーURL
-				tr.nullable
-					td bio
-					td String
-					td プロフィール
-				tr.nullable
-					td birthday
-					td String
-					td 誕生日(YYYY-MM-DD)
-				tr
-					td created_at
-					td Date
-					td アカウント作成日時
-				tr.optional
-					td drive_capacity
-					td Number
-					td ドライブの最大容量(byte単位)
-				tr
-					td followers_count
-					td Number
-					td フォロワー数
-				tr
-					td following_count
-					td Number
-					td フォロー数
-				tr
-					td id
-					td ID
-					td ユーザーID
-				tr.optional
-					td is_bot
-					td Boolean
-					td botかどうか
-				tr.optional
-					td is_followed
-					td Boolean
-					td フォローされているか
-				tr.optional
-					td is_following
-					td Boolean
-					td フォローしているか
-				tr
-					td liked_count
-					td Number
-					td 投稿にいいねされた数
-				tr
-					td likes_count
-					td Number
-					td 投稿にいいねした数
-				tr.nullable
-					td location
-					td String
-					td 住処
-				tr
-					td name
-					td String
-					td ニックネーム
-				tr
-					td posts_count
-					td Number
-					td 投稿数
-				tr
-					td username
-					td String
-					td ユーザー名
-
-	section
-		h2 Example
-		pre: code.
-			{
-				"avatar_id": "583ddc6e64df272771f74c1a",
-				"avatar_url": "https://file.himasaku.net/583ddc6e64df272771f74c1a",
-				"banner_id": "584bfc82d8e5186f8f755ec5",
-				"banner_url": "https://file.himasaku.net/584bfc82d8e5186f8f755ec5",
-				"bio": "どうすれば君だけのために生きていけるの",
-				"birthday": "1997-12-06",
-				"created_at": "2016-09-07T13:46:56.605Z",
-				"drive_capacity": 1073741824,
-				"email": null,
-				"followers_count": 51,
-				"following_count": 97,
-				"id": "57d01a501fdf2d07be417afe",
-				"liked_count": 750,
-				"likes_count": 3130,
-				"links": null,
-				"location": "川崎",
-				"name": "きな子",
-				"posts_count": 4811,
-				"username": "syuilo"
-			}
diff --git a/src/web/docs/api/entities/user.yaml b/src/web/docs/api/entities/user.yaml
index 9b1efd1fe..abc3f300d 100644
--- a/src/web/docs/api/entities/user.yaml
+++ b/src/web/docs/api/entities/user.yaml
@@ -65,6 +65,16 @@ props:
     desc:
       ja: "フォローしているユーザーの数"
       en: "The number of the following users for this user"
+  - name: "is_following"
+    type: "boolean"
+    optional: true
+    desc:
+      ja: "自分がこのユーザーをフォローしているか"
+  - name: "is_followed"
+    type: "boolean"
+    optional: true
+    desc:
+      ja: "自分がこのユーザーにフォローされているか"
   - name: "last_used_at"
     type: "date"
     optional: false

From 169b99a358f166185147970b916adf1a09d23de3 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 15 Dec 2017 06:41:57 +0900
Subject: [PATCH 086/186] :v:

---
 gulpfile.ts                         | 19 +-------
 src/web/docs/api/endpoints/view.pug |  3 +-
 src/web/docs/api/entities/view.pug  |  2 +-
 src/web/docs/api/gulpfile.ts        | 60 ++++++++++++++++---------
 src/web/docs/api/mixins.pug         |  6 +--
 src/web/docs/gulpfile.ts            | 64 ++++++++++++++++++++++++++
 src/web/docs/index.en.pug           |  9 ++++
 src/web/docs/index.ja.pug           |  9 ++++
 src/web/docs/index.md               |  4 --
 src/web/docs/layout.pug             | 23 +++++++---
 src/web/docs/style.styl             | 69 ++++++++++++++++-------------
 src/web/docs/vars.ts                | 36 +++++++++++++++
 12 files changed, 220 insertions(+), 84 deletions(-)
 create mode 100644 src/web/docs/gulpfile.ts
 create mode 100644 src/web/docs/index.en.pug
 create mode 100644 src/web/docs/index.ja.pug
 delete mode 100644 src/web/docs/index.md
 create mode 100644 src/web/docs/vars.ts

diff --git a/gulpfile.ts b/gulpfile.ts
index 6807b6d57..e7d477061 100644
--- a/gulpfile.ts
+++ b/gulpfile.ts
@@ -13,7 +13,6 @@ import * as es from 'event-stream';
 import cssnano = require('gulp-cssnano');
 import * as uglifyComposer from 'gulp-uglify/composer';
 import pug = require('gulp-pug');
-import stylus = require('gulp-stylus');
 import * as rimraf from 'rimraf';
 import chalk from 'chalk';
 import imagemin = require('gulp-imagemin');
@@ -48,32 +47,18 @@ if (isDebug) {
 
 const constants = require('./src/const.json');
 
-require('./src/web/docs/api/gulpfile.ts');
+require('./src/web/docs/gulpfile.ts');
 
 gulp.task('build', [
 	'build:js',
 	'build:ts',
 	'build:copy',
 	'build:client',
-	'build:doc'
+	'doc'
 ]);
 
 gulp.task('rebuild', ['clean', 'build']);
 
-gulp.task('build:doc', [
-	'doc:api',
-	'doc:styles'
-]);
-
-gulp.task('doc:styles', () =>
-	gulp.src('./src/web/docs/**/*.styl')
-		.pipe(stylus())
-		.pipe(isProduction
-			? (cssnano as any)()
-			: gutil.noop())
-		.pipe(gulp.dest('./built/web/assets/docs/'))
-);
-
 gulp.task('build:js', () =>
 	gulp.src(['./src/**/*.js', '!./src/web/**/*.js'])
 		.pipe(gulp.dest('./built/'))
diff --git a/src/web/docs/api/endpoints/view.pug b/src/web/docs/api/endpoints/view.pug
index cebef9fa5..cab814cab 100644
--- a/src/web/docs/api/endpoints/view.pug
+++ b/src/web/docs/api/endpoints/view.pug
@@ -12,7 +12,7 @@ block main
 
 	p#url= url
 
-	p#desc: +i18n(desc)
+	p#desc= desc[lang] || desc['ja']
 
 	section
 		h2 Params
@@ -27,4 +27,3 @@ block main
 	section
 		h2 Response
 		+propTable(res)
-
diff --git a/src/web/docs/api/entities/view.pug b/src/web/docs/api/entities/view.pug
index f210582f1..756e966b5 100644
--- a/src/web/docs/api/entities/view.pug
+++ b/src/web/docs/api/entities/view.pug
@@ -10,7 +10,7 @@ block meta
 block main
 	h1= name
 
-	p#desc: +i18n(desc)
+	p#desc= desc[lang] || desc['ja']
 
 	section
 		h2 Properties
diff --git a/src/web/docs/api/gulpfile.ts b/src/web/docs/api/gulpfile.ts
index 6453996d3..6cbae5ea2 100644
--- a/src/web/docs/api/gulpfile.ts
+++ b/src/web/docs/api/gulpfile.ts
@@ -12,6 +12,12 @@ import * as mkdirp from 'mkdirp';
 
 import config from './../../../conf';
 
+import generateVars from '../vars';
+
+const commonVars = generateVars();
+
+const langs = ['ja', 'en'];
+
 const kebab = string => string.replace(/([a-z])([A-Z])/g, '$1-$2').replace(/\s+/g, '-').toLowerCase();
 
 const parseParam = param => {
@@ -102,20 +108,25 @@ gulp.task('doc:api:endpoints', () => {
 				paramDefs: extractDefs(ep.params),
 				res: sortParams(ep.res.map(p => parseParam(p))),
 				resDefs: extractDefs(ep.res),
-				kebab
+				kebab,
+				common: commonVars
 			};
-			pug.renderFile('./src/web/docs/api/endpoints/view.pug', vars, (renderErr, html) => {
-				if (renderErr) {
-					console.error(renderErr);
-					return;
-				}
-				const htmlPath = `./built/web/docs/api/endpoints/${ep.endpoint}.html`;
-				mkdirp(path.dirname(htmlPath), (mkdirErr) => {
-					if (mkdirErr) {
-						console.error(mkdirErr);
+			langs.forEach(lang => {
+				pug.renderFile('./src/web/docs/api/endpoints/view.pug', Object.assign({}, vars, {
+					lang
+				}), (renderErr, html) => {
+					if (renderErr) {
+						console.error(renderErr);
 						return;
 					}
-					fs.writeFileSync(htmlPath, html, 'utf-8');
+					const htmlPath = `./built/web/docs/${lang}/api/endpoints/${ep.endpoint}.html`;
+					mkdirp(path.dirname(htmlPath), (mkdirErr) => {
+						if (mkdirErr) {
+							console.error(mkdirErr);
+							return;
+						}
+						fs.writeFileSync(htmlPath, html, 'utf-8');
+					});
 				});
 			});
 		});
@@ -135,20 +146,25 @@ gulp.task('doc:api:entities', () => {
 				desc: entity.desc,
 				props: sortParams(entity.props.map(p => parseParam(p))),
 				propDefs: extractDefs(entity.props),
-				kebab
+				kebab,
+				common: commonVars
 			};
-			pug.renderFile('./src/web/docs/api/entities/view.pug', vars, (renderErr, html) => {
-				if (renderErr) {
-					console.error(renderErr);
-					return;
-				}
-				const htmlPath = `./built/web/docs/api/entities/${kebab(entity.name)}.html`;
-				mkdirp(path.dirname(htmlPath), (mkdirErr) => {
-					if (mkdirErr) {
-						console.error(mkdirErr);
+			langs.forEach(lang => {
+				pug.renderFile('./src/web/docs/api/entities/view.pug', Object.assign({}, vars, {
+					lang
+				}), (renderErr, html) => {
+					if (renderErr) {
+						console.error(renderErr);
 						return;
 					}
-					fs.writeFileSync(htmlPath, html, 'utf-8');
+					const htmlPath = `./built/web/docs/${lang}/api/entities/${kebab(entity.name)}.html`;
+					mkdirp(path.dirname(htmlPath), (mkdirErr) => {
+						if (mkdirErr) {
+							console.error(mkdirErr);
+							return;
+						}
+						fs.writeFileSync(htmlPath, html, 'utf-8');
+					});
 				});
 			});
 		});
diff --git a/src/web/docs/api/mixins.pug b/src/web/docs/api/mixins.pug
index b302c7826..3ddd7cb48 100644
--- a/src/web/docs/api/mixins.pug
+++ b/src/web/docs/api/mixins.pug
@@ -14,13 +14,13 @@ mixin propTable(props)
 						if prop.kind == 'id'
 							if prop.entity
 								|  (
-								a(href=`/docs/api/entities/${kebab(prop.entity)}`)= prop.entity
+								a(href=`/docs/${lang}/api/entities/${kebab(prop.entity)}`)= prop.entity
 								|  ID)
 							else
 								|  (ID)
 						else if prop.kind == 'entity'
 							|   (
-							a(href=`/docs/api/entities/${kebab(prop.entity)}`)= prop.entity
+							a(href=`/docs/${lang}/api/entities/${kebab(prop.entity)}`)= prop.entity
 							| )
 						else if prop.kind == 'object'
 							if prop.def
@@ -30,4 +30,4 @@ mixin propTable(props)
 						else if prop.kind == 'date'
 							|  (Date)
 					td.optional= prop.optional.toString()
-					td.desc: +i18n(prop.desc)
+					td.desc!= prop.desc[lang] || prop.desc['ja']
diff --git a/src/web/docs/gulpfile.ts b/src/web/docs/gulpfile.ts
new file mode 100644
index 000000000..6f2351dac
--- /dev/null
+++ b/src/web/docs/gulpfile.ts
@@ -0,0 +1,64 @@
+/**
+ * Gulp tasks
+ */
+
+import * as fs from 'fs';
+import * as path from 'path';
+import * as glob from 'glob';
+import * as gulp from 'gulp';
+import * as pug from 'pug';
+//import * as yaml from 'js-yaml';
+import * as mkdirp from 'mkdirp';
+import stylus = require('gulp-stylus');
+import cssnano = require('gulp-cssnano');
+
+//import config from './../../conf';
+
+import generateVars from './vars';
+
+require('./api/gulpfile.ts');
+
+gulp.task('doc', [
+	'doc:docs',
+	'doc:api',
+	'doc:styles'
+]);
+
+const commonVars = generateVars();
+
+gulp.task('doc:docs', () => {
+	glob('./src/web/docs/**/*.*.pug', (globErr, files) => {
+		if (globErr) {
+			console.error(globErr);
+			return;
+		}
+		files.forEach(file => {
+			const [, name, lang] = file.match(/docs\/(.+?)\.(.+?)\.pug$/);
+			const vars = {
+				common: commonVars,
+				lang: lang
+			};
+			pug.renderFile(file, vars, (renderErr, html) => {
+				if (renderErr) {
+					console.error(renderErr);
+					return;
+				}
+				const htmlPath = `./built/web/docs/${lang}/${name}.html`;
+				mkdirp(path.dirname(htmlPath), (mkdirErr) => {
+					if (mkdirErr) {
+						console.error(mkdirErr);
+						return;
+					}
+					fs.writeFileSync(htmlPath, html, 'utf-8');
+				});
+			});
+		});
+	});
+});
+
+gulp.task('doc:styles', () =>
+	gulp.src('./src/web/docs/**/*.styl')
+		.pipe(stylus())
+		.pipe((cssnano as any)())
+		.pipe(gulp.dest('./built/web/assets/docs/'))
+);
diff --git a/src/web/docs/index.en.pug b/src/web/docs/index.en.pug
new file mode 100644
index 000000000..af0bba8b2
--- /dev/null
+++ b/src/web/docs/index.en.pug
@@ -0,0 +1,9 @@
+extends ./layout.pug
+
+block title
+	| Misskey Docs
+
+block main
+	h1 Misskey Docs
+
+	p Welcome to docs of Misskey.
diff --git a/src/web/docs/index.ja.pug b/src/web/docs/index.ja.pug
new file mode 100644
index 000000000..cd43045f6
--- /dev/null
+++ b/src/web/docs/index.ja.pug
@@ -0,0 +1,9 @@
+extends ./layout.pug
+
+block title
+	| Misskey ドキュメント
+
+block main
+	h1 Misskey ドキュメント
+
+	p Misskeyのドキュメントへようこそ
diff --git a/src/web/docs/index.md b/src/web/docs/index.md
deleted file mode 100644
index 0846cf27e..000000000
--- a/src/web/docs/index.md
+++ /dev/null
@@ -1,4 +0,0 @@
-Misskeyについて
-================================================================
-
-誰か書いて
diff --git a/src/web/docs/layout.pug b/src/web/docs/layout.pug
index 68ca9eb62..d6ecb4b6a 100644
--- a/src/web/docs/layout.pug
+++ b/src/web/docs/layout.pug
@@ -1,16 +1,29 @@
 doctype html
 
-mixin i18n(xs)
-	each text, lang in xs
-		span(class=`i18n ${lang}`)!= text
-
-html
+html(lang= lang)
 	head
 		meta(charset="UTF-8")
+		meta(name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no")
 		title
 			block title
+		link(rel="stylesheet" href="/assets/docs/style.css")
 		block meta
 
 	body
+		nav
+			ul
+				each doc in common.docs
+					li: a(href=`/docs/${lang}/${doc.name}`)= doc.title[lang] || doc.title['ja']
+			section
+				h2 API
+				ul
+					li Entities
+						ul
+							each entity in common.entities
+								li: a(href=`/docs/${lang}/api/entities/${common.kebab(entity)}`)= entity
+					li Endpoints
+						ul
+							each endpoint in common.endpoints
+								li: a(href=`/docs/${lang}/api/endpoints/${common.kebab(endpoint)}`)= endpoint
 		main
 			block main
diff --git a/src/web/docs/style.styl b/src/web/docs/style.styl
index a4abc5a9a..2e2f9f574 100644
--- a/src/web/docs/style.styl
+++ b/src/web/docs/style.styl
@@ -5,10 +5,49 @@ body
 	color #34495e
 
 main
+	margin 0 0 0 256px
 	padding 32px
 	width 100%
 	max-width 700px
 
+	section
+		margin 32px 0
+
+	h1
+		margin 0 0 24px 0
+		padding 16px 0
+		font-size 1.5em
+		border-bottom solid 2px #eee
+
+	h2
+		margin 0 0 24px 0
+		padding 0 0 16px 0
+		font-size 1.4em
+		border-bottom solid 1px #eee
+
+	h3
+		margin 0
+		padding 0
+		font-size 1.25em
+
+	h4
+		margin 0
+
+	p
+		margin 1em 0
+		line-height 1.6em
+
+nav
+	display block
+	position fixed
+	top 0
+	left 0
+	width 256px
+	height 100%
+	overflow auto
+	padding 32px
+	border-right solid 2px #eee
+
 footer
 	padding:32px 0 0 0
 	margin 32px 0 0 0
@@ -18,33 +57,6 @@ footer
 		margin 16px 0 0 0
 		color #aaa
 
-section
-	margin 32px 0
-
-h1
-	margin 0 0 24px 0
-	padding 16px 0
-	font-size 1.5em
-	border-bottom solid 2px #eee
-
-h2
-	margin 0 0 24px 0
-	padding 0 0 16px 0
-	font-size 1.4em
-	border-bottom solid 1px #eee
-
-h3
-	margin 0
-	padding 0
-	font-size 1.25em
-
-h4
-	margin 0
-
-p
-	margin 1em 0
-	line-height 1.6em
-
 table
 	width 100%
 	border-spacing 0
@@ -72,6 +84,3 @@ table
 
 	th, td
 		padding 8px 16px
-
-.i18n:not(.ja)
-	display none
diff --git a/src/web/docs/vars.ts b/src/web/docs/vars.ts
new file mode 100644
index 000000000..ed2149df4
--- /dev/null
+++ b/src/web/docs/vars.ts
@@ -0,0 +1,36 @@
+import * as fs from 'fs';
+import * as glob from 'glob';
+import * as yaml from 'js-yaml';
+
+export default function() {
+	const vars = {};
+
+	const endpoints = glob.sync('./src/web/docs/api/endpoints/**/*.yaml');
+	vars['endpoints'] = endpoints.map(ep => {
+		const _ep = yaml.safeLoad(fs.readFileSync(ep, 'utf-8'));
+		return _ep.endpoint;
+	});
+
+	const entities = glob.sync('./src/web/docs/api/entities/**/*.yaml');
+	vars['entities'] = entities.map(x => {
+		const _x = yaml.safeLoad(fs.readFileSync(x, 'utf-8'));
+		return _x.name;
+	});
+
+	const docs = glob.sync('./src/web/docs/**/*.*.pug');
+	vars['docs'] = {};
+	docs.forEach(x => {
+		const [, name, lang] = x.match(/docs\/(.+?)\.(.+?)\.pug$/);
+		if (vars['docs'][name] == null) {
+			vars['docs'][name] = {
+				name,
+				title: {}
+			};
+		}
+		vars['docs'][name]['title'][lang] = fs.readFileSync(x, 'utf-8').match(/\r\n\th1 (.+?)\r\n/)[1];
+	});
+
+	vars['kebab'] = string => string.replace(/([a-z])([A-Z])/g, '$1-$2').replace(/\s+/g, '-').toLowerCase();
+
+	return vars;
+}

From fae40ad1dde72f09ecd0e0630b4e757cf9758010 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 15 Dec 2017 06:45:19 +0900
Subject: [PATCH 087/186] :v:

---
 src/web/docs/vars.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/web/docs/vars.ts b/src/web/docs/vars.ts
index ed2149df4..80fdc9a7d 100644
--- a/src/web/docs/vars.ts
+++ b/src/web/docs/vars.ts
@@ -27,7 +27,7 @@ export default function() {
 				title: {}
 			};
 		}
-		vars['docs'][name]['title'][lang] = fs.readFileSync(x, 'utf-8').match(/\r\n\th1 (.+?)\r\n/)[1];
+		vars['docs'][name]['title'][lang] = fs.readFileSync(x, 'utf-8').match(/\r?\n\th1 (.+?)\r?\n/)[1];
 	});
 
 	vars['kebab'] = string => string.replace(/([a-z])([A-Z])/g, '$1-$2').replace(/\s+/g, '-').toLowerCase();

From 3bcdbe151f89622aabc9c38a8a4d69abdec97619 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=93=E3=81=B4=E3=81=AA=E3=81=9F=E3=81=BF=E3=81=BD?=
 <syuilotan@yahoo.co.jp>
Date: Fri, 15 Dec 2017 09:03:22 +0900
Subject: [PATCH 088/186] :v:

---
 src/web/docs/api/entities/drive-file.yaml | 73 +++++++++++++++++++++++
 1 file changed, 73 insertions(+)
 create mode 100644 src/web/docs/api/entities/drive-file.yaml

diff --git a/src/web/docs/api/entities/drive-file.yaml b/src/web/docs/api/entities/drive-file.yaml
new file mode 100644
index 000000000..2ebbb089a
--- /dev/null
+++ b/src/web/docs/api/entities/drive-file.yaml
@@ -0,0 +1,73 @@
+name: "DriveFile"
+
+desc:
+  ja: "ドライブのファイル。"
+  en: "A file of Drive."
+
+props:
+  - name: "id"
+    type: "id"
+    optional: false
+    desc:
+      ja: "ファイルID"
+      en: "The ID of this file"
+  - name: "created_at"
+    type: "date"
+    optional: false
+    desc:
+      ja: "アップロード日時"
+      en: "The upload date of this file"
+  - name: "user_id"
+    type: "id(User)"
+    optional: false
+    desc:
+      ja: "所有者ID"
+      en: "The ID of the owner of this file"
+  - name: "user"
+    type: "entity(User)"
+    optional: true
+    desc:
+      ja: "所有者"
+      en: "The owner of this file"
+  - name: "name"
+    type: "string"
+    optional: false
+    desc:
+      ja: "ファイル名"
+      en: "The name of this file"
+  - name: "md5"
+    type: "string"
+    optional: false
+    desc:
+      ja: "ファイルのMD5ハッシュ値"
+      en: "The md5 hash value of this file"
+  - name: "type"
+    type: "string"
+    optional: false
+    desc:
+      ja: "ファイルの種類"
+      en: "The type of this file"
+  - name: "datasize"
+    type: "number"
+    optional: false
+    desc:
+      ja: "ファイルサイズ(bytes)"
+      en: "The size of this file (bytes)"
+  - name: "url"
+    type: "string"
+    optional: false
+    desc:
+      ja: "ファイルのURL"
+      en: "The URL of this file"
+  - name: "folder_id"
+    type: "id(DriveFolder)"
+    optional: true
+    desc:
+      ja: "フォルダID"
+      en: "The ID of the folder of this file"
+  - name: "folder"
+    type: "entity(DriveFolder)"
+    optional: true
+    desc:
+      ja: "フォルダ"
+      en: "The folder of this file"

From e1cc715589f83147b64a76bf0962b7a77dd2d19c Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 16 Dec 2017 00:19:10 +0900
Subject: [PATCH 089/186] :v:

---
 src/web/docs/api/endpoints/view.pug |  3 ---
 src/web/docs/api/entities/view.pug  |  3 ---
 src/web/docs/api/gulpfile.ts        | 14 ++++++++------
 src/web/docs/gulpfile.ts            | 24 +++++++++++++++++-------
 src/web/docs/index.en.pug           | 10 ++--------
 src/web/docs/index.ja.pug           | 10 ++--------
 src/web/docs/layout.pug             |  4 +++-
 src/web/docs/vars.ts                |  2 +-
 8 files changed, 33 insertions(+), 37 deletions(-)

diff --git a/src/web/docs/api/endpoints/view.pug b/src/web/docs/api/endpoints/view.pug
index cab814cab..d456022f6 100644
--- a/src/web/docs/api/endpoints/view.pug
+++ b/src/web/docs/api/endpoints/view.pug
@@ -1,9 +1,6 @@
 extends ../../layout.pug
 include ../mixins
 
-block title
-	| #{endpoint} | Misskey API
-
 block meta
 	link(rel="stylesheet" href="/assets/docs/api/endpoints/style.css")
 
diff --git a/src/web/docs/api/entities/view.pug b/src/web/docs/api/entities/view.pug
index 756e966b5..57c6d4cad 100644
--- a/src/web/docs/api/entities/view.pug
+++ b/src/web/docs/api/entities/view.pug
@@ -1,9 +1,6 @@
 extends ../../layout.pug
 include ../mixins
 
-block title
-	| #{name} | Misskey API
-
 block meta
 	link(rel="stylesheet" href="/assets/docs/api/entities/style.css")
 
diff --git a/src/web/docs/api/gulpfile.ts b/src/web/docs/api/gulpfile.ts
index 6cbae5ea2..139ae9241 100644
--- a/src/web/docs/api/gulpfile.ts
+++ b/src/web/docs/api/gulpfile.ts
@@ -108,12 +108,13 @@ gulp.task('doc:api:endpoints', () => {
 				paramDefs: extractDefs(ep.params),
 				res: sortParams(ep.res.map(p => parseParam(p))),
 				resDefs: extractDefs(ep.res),
-				kebab,
-				common: commonVars
 			};
 			langs.forEach(lang => {
 				pug.renderFile('./src/web/docs/api/endpoints/view.pug', Object.assign({}, vars, {
-					lang
+					lang,
+					title: ep.endpoint,
+					kebab,
+					common: commonVars
 				}), (renderErr, html) => {
 					if (renderErr) {
 						console.error(renderErr);
@@ -146,12 +147,13 @@ gulp.task('doc:api:entities', () => {
 				desc: entity.desc,
 				props: sortParams(entity.props.map(p => parseParam(p))),
 				propDefs: extractDefs(entity.props),
-				kebab,
-				common: commonVars
 			};
 			langs.forEach(lang => {
 				pug.renderFile('./src/web/docs/api/entities/view.pug', Object.assign({}, vars, {
-					lang
+					lang,
+					title: entity.name,
+					kebab,
+					common: commonVars
 				}), (renderErr, html) => {
 					if (renderErr) {
 						console.error(renderErr);
diff --git a/src/web/docs/gulpfile.ts b/src/web/docs/gulpfile.ts
index 6f2351dac..237784465 100644
--- a/src/web/docs/gulpfile.ts
+++ b/src/web/docs/gulpfile.ts
@@ -36,20 +36,30 @@ gulp.task('doc:docs', () => {
 			const [, name, lang] = file.match(/docs\/(.+?)\.(.+?)\.pug$/);
 			const vars = {
 				common: commonVars,
-				lang: lang
+				lang: lang,
+				title: fs.readFileSync(file, 'utf-8').match(/^h1 (.+?)\r?\n/)[1]
 			};
-			pug.renderFile(file, vars, (renderErr, html) => {
+			pug.renderFile(file, vars, (renderErr, content) => {
 				if (renderErr) {
 					console.error(renderErr);
 					return;
 				}
-				const htmlPath = `./built/web/docs/${lang}/${name}.html`;
-				mkdirp(path.dirname(htmlPath), (mkdirErr) => {
-					if (mkdirErr) {
-						console.error(mkdirErr);
+
+				pug.renderFile('./src/web/docs/layout.pug', Object.assign({}, vars, {
+					content
+				}), (renderErr2, html) => {
+					if (renderErr2) {
+						console.error(renderErr2);
 						return;
 					}
-					fs.writeFileSync(htmlPath, html, 'utf-8');
+					const htmlPath = `./built/web/docs/${lang}/${name}.html`;
+					mkdirp(path.dirname(htmlPath), (mkdirErr) => {
+						if (mkdirErr) {
+							console.error(mkdirErr);
+							return;
+						}
+						fs.writeFileSync(htmlPath, html, 'utf-8');
+					});
 				});
 			});
 		});
diff --git a/src/web/docs/index.en.pug b/src/web/docs/index.en.pug
index af0bba8b2..1fcc870d3 100644
--- a/src/web/docs/index.en.pug
+++ b/src/web/docs/index.en.pug
@@ -1,9 +1,3 @@
-extends ./layout.pug
+h1 Misskey Docs
 
-block title
-	| Misskey Docs
-
-block main
-	h1 Misskey Docs
-
-	p Welcome to docs of Misskey.
+p Welcome to docs of Misskey.
diff --git a/src/web/docs/index.ja.pug b/src/web/docs/index.ja.pug
index cd43045f6..4a0bf7fa1 100644
--- a/src/web/docs/index.ja.pug
+++ b/src/web/docs/index.ja.pug
@@ -1,9 +1,3 @@
-extends ./layout.pug
+h1 Misskey ドキュメント
 
-block title
-	| Misskey ドキュメント
-
-block main
-	h1 Misskey ドキュメント
-
-	p Misskeyのドキュメントへようこそ
+p Misskeyのドキュメントへようこそ
diff --git a/src/web/docs/layout.pug b/src/web/docs/layout.pug
index d6ecb4b6a..f8570dd3a 100644
--- a/src/web/docs/layout.pug
+++ b/src/web/docs/layout.pug
@@ -5,7 +5,7 @@ html(lang= lang)
 		meta(charset="UTF-8")
 		meta(name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no")
 		title
-			block title
+			| #{title} | Misskey Docs
 		link(rel="stylesheet" href="/assets/docs/style.css")
 		block meta
 
@@ -27,3 +27,5 @@ html(lang= lang)
 								li: a(href=`/docs/${lang}/api/endpoints/${common.kebab(endpoint)}`)= endpoint
 		main
 			block main
+			if content
+				| !{content}
diff --git a/src/web/docs/vars.ts b/src/web/docs/vars.ts
index 80fdc9a7d..37bc9d7b0 100644
--- a/src/web/docs/vars.ts
+++ b/src/web/docs/vars.ts
@@ -27,7 +27,7 @@ export default function() {
 				title: {}
 			};
 		}
-		vars['docs'][name]['title'][lang] = fs.readFileSync(x, 'utf-8').match(/\r?\n\th1 (.+?)\r?\n/)[1];
+		vars['docs'][name]['title'][lang] = fs.readFileSync(x, 'utf-8').match(/^h1 (.+?)\r?\n/)[1];
 	});
 
 	vars['kebab'] = string => string.replace(/([a-z])([A-Z])/g, '$1-$2').replace(/\s+/g, '-').toLowerCase();

From 1a733f6b7128aed69fee2993eda1ec5f4ff9d79c Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 16 Dec 2017 01:06:28 +0900
Subject: [PATCH 090/186] :v:

---
 src/web/docs/api.ja.pug | 66 +++++++++++++++++++++++++++++++++++++++++
 src/web/docs/style.styl |  2 --
 src/web/docs/vars.ts    |  3 ++
 3 files changed, 69 insertions(+), 2 deletions(-)
 create mode 100644 src/web/docs/api.ja.pug

diff --git a/src/web/docs/api.ja.pug b/src/web/docs/api.ja.pug
new file mode 100644
index 000000000..588b926bd
--- /dev/null
+++ b/src/web/docs/api.ja.pug
@@ -0,0 +1,66 @@
+h1 Misskey API
+
+p MisskeyはWeb APIを公開しており、アプリケーションから様々な操作を行うことができます。
+
+section
+	h2 自分の所有するアカウントからAPIにアクセスする場合
+	p 「設定」で、APIにアクセスするのに必要なAPIキーを取得してください。
+	p APIにアクセスする際には、リクエストにAPIキーを「i」というパラメータ名で含めます。
+	p APIの詳しい使用法は「Misskey APIの利用」セクションをご覧ください。
+
+section
+	h2 アプリケーションからAPIにアクセスする場合
+	p
+		| あなたのWebサービスやアプリケーションなどからMisskey APIを利用したい場合、
+		| ユーザーにアカウントへのアクセスを許可してもらい、ユーザーのアクセストークンを取得する必要があります。
+	p アクセストークンを取得するまでの流れを説明します。
+
+	section
+		h3 1.アプリケーションを登録する
+		p まず、あなたのWebサービスやアプリケーションをMisskeyに登録します。
+		p デベロッパーセンターから登録を行ってください。
+		p 登録が済むとアプリケーションのシークレットキーが入手できます。
+
+	section
+		h3 2.ユーザーに認証させる
+		p あなたのアプリケーションを使ってもらうには、ユーザーにアカウントへのアクセスの許可をもらう必要があります。
+		p
+			| 認証セッションを開始するには、#{common.config.api_url}/auth/session/generate へパラメータに app_secret としてシークレットキーを含めたリクエストを送信します。
+			| リクエスト形式はJSONで、メソッドはPOSTです。
+			| レスポンスとして認証セッションのトークンや認証フォームのURLが取得できるので、認証フォームのURLをブラウザで表示し、ユーザーにフォームを提示してください。
+
+		p
+			| あなたのアプリがコールバックURLを設定している場合、
+			| ユーザーがアプリの連携を許可すると設定しているコールバックURLに token という名前でセッションのトークンが含まれたクエリを付けてリダイレクトします。
+
+		p
+			| あなたのアプリがコールバックURLを設定していない場合、ユーザーがアプリの連携を許可したことを(何らかの方法で(たとえばボタンを押させるなど))確認出来るようにしてください。
+
+	section
+		h3 3.ユーザーのアクセストークンを取得する
+		p ユーザーが連携を許可したら、#{common.config.api_url}/auth/session/userkey へ次のパラメータを含むリクエストを送信します:
+		table
+			thead
+				tr
+					th 名前
+					th 型
+					th 説明
+			tbody
+				tr
+					td app_secret
+					td string
+					td アプリのシークレットキー
+				tr
+					td token
+					td string
+					td セッションのトークン
+		p 上手くいけば、認証したユーザーのアクセストークンがレスポンスとして取得できます。おめでとうございます!
+
+	p アクセストークンが取得できたら、「ユーザーのアクセストークン+アプリのシークレットキーをsha256したもの」を「i」というパラメータでリクエストに含めるだけで、APIにアクセスできます。
+
+	p APIの詳しい使用法は「Misskey APIの利用」セクションをご覧ください。
+
+section
+	h2 Misskey APIの利用
+	p APIはすべてリクエストのパラメータ・レスポンスともにJSON形式です。また、すべてのエンドポイントはPOSTメソッドのみ受け付けます。
+	p APIリファレンスもご確認ください。
diff --git a/src/web/docs/style.styl b/src/web/docs/style.styl
index 2e2f9f574..f222e65bf 100644
--- a/src/web/docs/style.styl
+++ b/src/web/docs/style.styl
@@ -77,8 +77,6 @@ table
 
 	tbody
 		tr
-			border-bottom dashed 1px #eee
-
 			&:nth-child(odd)
 				background #fbfbfb
 
diff --git a/src/web/docs/vars.ts b/src/web/docs/vars.ts
index 37bc9d7b0..ffa262a06 100644
--- a/src/web/docs/vars.ts
+++ b/src/web/docs/vars.ts
@@ -1,6 +1,7 @@
 import * as fs from 'fs';
 import * as glob from 'glob';
 import * as yaml from 'js-yaml';
+import config from '../../conf';
 
 export default function() {
 	const vars = {};
@@ -32,5 +33,7 @@ export default function() {
 
 	vars['kebab'] = string => string.replace(/([a-z])([A-Z])/g, '$1-$2').replace(/\s+/g, '-').toLowerCase();
 
+	vars['config'] = config;
+
 	return vars;
 }

From 42dad3873f863762262a4abfef2daa479ecc0bc0 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 16 Dec 2017 04:10:05 +0900
Subject: [PATCH 091/186] :v:

---
 src/web/docs/api/getting-started.md | 73 -----------------------------
 src/web/docs/api/library.md         |  8 ----
 src/web/docs/layout.pug             |  9 ++--
 src/web/docs/link-to-twitter.md     |  9 ----
 4 files changed, 5 insertions(+), 94 deletions(-)
 delete mode 100644 src/web/docs/api/getting-started.md
 delete mode 100644 src/web/docs/api/library.md
 delete mode 100644 src/web/docs/link-to-twitter.md

diff --git a/src/web/docs/api/getting-started.md b/src/web/docs/api/getting-started.md
deleted file mode 100644
index e13659914..000000000
--- a/src/web/docs/api/getting-started.md
+++ /dev/null
@@ -1,73 +0,0 @@
-Getting Started
-================================================================
-MisskeyはREST APIやStreaming APIを提供しており、プログラムからMisskeyの全ての機能を利用することができます。
-それらのAPIを利用するには、まずAPIを利用したいアカウントのアクセストークンを取得する必要があります:
-
-自分のアクセストークンを取得したい場合
-----------------------------------------------------------------
-自分自身のアクセストークンは、設定 > API で確認できます。
-<p class="tip">
-	アカウントを乗っ取られてしまう可能性があるため、トークンは第三者に教えないでください(アプリなどにも入力しないでください)。<br>
-	万が一トークンが漏れたりその可能性がある場合は トークンを再生成できます。(副作用として、ログインしているすべてのデバイスでログアウトが発生します)
-</p>
-
-他人のアクセストークンを取得する
-----------------------------------------------------------------
-不特定多数のユーザーからAPIを利用したい場合、アプリケーションを作成します。
-アプリケーションを作成すると、ユーザーが連携を許可した時に、そのユーザーのアクセストークンを取得することができます。
-
-アプリケーションを作成してアクセストークンを取得するまでの流れを説明します。
-
-### アプリケーションを作成する
-まずはあなたのアプリケーションを作成しましょう。
-				| <a href=#{dev_url} target="_blank">デベロッパーセンター</a>にアクセスし、アプリ > アプリ作成 に進みます。
-				br
-				| 次に、フォームに必要事項を記入します:
-			dl
-				dt アプリケーション名
-				dd あなたのアプリケーションの名前。
-				dt Named ID
-				dd アプリを識別する/a-z-/で構成されたID。
-				dt アプリの概要
-				dd アプリの簡単な説明を入力してください。
-				dt コールバックURL
-				dd あなたのアプリケーションがWebアプリケーションである場合、ユーザーが後述するフォームで認証を終えた際にリダイレクトするURLを設定できます。
-				dt 権限
-				dd アプリケーションが要求する権限。ここで要求した機能だけがAPIからアクセスできます。
-			p.tip
-				| 権限はアプリ作成後も変更できますが、新たな権限を付与する場合、その時点で関連付けられているユーザーはすべて無効になります。
-			p
-				| アプリケーションを作成すると、作ったアプリの管理ページに進みます。
-				br
-				| アプリのシークレットキー(App Secret)が表示されていますので、メモしておいてください。
-			p.tip
-				| アプリに成りすまされる可能性があるため、極力このシークレットキーは公開しないようにしてください。
-
-		section
-			h3 ユーザーに認証させる
-			p あなたのアプリを使ってもらうには、ユーザーにアカウントへアクセスすることを許可してもらい、Misskeyにそのユーザーのアクセストークンを発行してもらう必要があります。
-			p 認証セッションを開始するには、<code>#{api_url}/auth/session/generate</code>へパラメータに<code>app_secret</code>としてApp Secretを含めたリクエストを送信します。
-			p
-				| そうすると、レスポンスとして認証セッションのトークンや認証フォームのURLが取得できます。
-				br
-				| この認証フォームのURLをブラウザで表示し、ユーザーにフォームを表示してください。
-			section
-				h4 あなたのアプリがコールバックURLを設定している場合
-				p ユーザーがアプリの連携を許可すると設定しているコールバックURLに<code>token</code>という名前でセッションのトークンが含まれたクエリを付けてリダイレクトします。
-			section
-				h4 あなたのアプリがコールバックURLを設定していない場合
-				p ユーザーがアプリの連携を許可したことを(何らかの方法で(たとえばボタンを押させるなど))確認出来るようにしてください。
-			p
-				| 次に、<code>#{api_url}/auth/session/userkey</code>へ<code>app_secret</code>としてApp Secretを、<code>token</code>としてセッションのトークンをパラメータとして付与したリクエストを送信してください。
-				br
-				| 上手くいけば、認証したユーザーのアクセストークンがレスポンスとして取得できます。おめでとうございます!
-			p
-				| 以降アクセストークンは、<strong>ユーザーのアクセストークン+アプリのシークレットキーをsha256したもの</strong>として扱います。
-
-	p アクセストークンを取得できたら、あとは簡単です。REST APIなら、リクエストにアクセストークンを<code>i</code>としてパラメータに含めるだけです。
-
-	section
-		h2 リクエスト形式
-		p <code>application/json</code>を受け付けます。
-		p.tip
-			| 現在<code>application/x-www-form-urlencoded</code>も受け付けていますが、将来的にこのサポートはされなくなる予定です。
diff --git a/src/web/docs/api/library.md b/src/web/docs/api/library.md
deleted file mode 100644
index 71ddbe345..000000000
--- a/src/web/docs/api/library.md
+++ /dev/null
@@ -1,8 +0,0 @@
-ライブラリ
-================================================================
-
-Misskey APIを便利に利用するためのライブラリ一覧です。
-
-.NET
-----------------------------------------------------------------
-* **[Misq (公式)](https://github.com/syuilo/Misq)**
diff --git a/src/web/docs/layout.pug b/src/web/docs/layout.pug
index f8570dd3a..ac3743d2f 100644
--- a/src/web/docs/layout.pug
+++ b/src/web/docs/layout.pug
@@ -3,28 +3,29 @@ doctype html
 html(lang= lang)
 	head
 		meta(charset="UTF-8")
-		meta(name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no")
+		meta(name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1, user-scalable=no")
 		title
 			| #{title} | Misskey Docs
 		link(rel="stylesheet" href="/assets/docs/style.css")
 		block meta
+		base(href=`/docs/${lang}/`)
 
 	body
 		nav
 			ul
 				each doc in common.docs
-					li: a(href=`/docs/${lang}/${doc.name}`)= doc.title[lang] || doc.title['ja']
+					li: a(href=`./${doc.name}`)= doc.title[lang] || doc.title['ja']
 			section
 				h2 API
 				ul
 					li Entities
 						ul
 							each entity in common.entities
-								li: a(href=`/docs/${lang}/api/entities/${common.kebab(entity)}`)= entity
+								li: a(href=`./api/entities/${common.kebab(entity)}`)= entity
 					li Endpoints
 						ul
 							each endpoint in common.endpoints
-								li: a(href=`/docs/${lang}/api/endpoints/${common.kebab(endpoint)}`)= endpoint
+								li: a(href=`./api/endpoints/${common.kebab(endpoint)}`)= endpoint
 		main
 			block main
 			if content
diff --git a/src/web/docs/link-to-twitter.md b/src/web/docs/link-to-twitter.md
deleted file mode 100644
index 77fb74457..000000000
--- a/src/web/docs/link-to-twitter.md
+++ /dev/null
@@ -1,9 +0,0 @@
-Twitterと連携する
-================================================================
-
-設定 -> Twitter から、お使いのMisskeyアカウントとお使いのTwitterアカウントを関連付けることができます。
-アカウントの関連付けを行うと、プロフィールにTwitterアカウントへのリンクが表示されたりなどします。
-
-MisskeyがあなたのTwitterアカウントでツイートしたり誰かをフォローしたりといったことは、
-一切行いませんのでご安心ください。(Misskeyはそのような権限を取得しないので、行おうと思っても行えません)
-Twitterのアプリケーション認証フォームでこの権限の詳細を確認することができます。また、いつでも連携を取り消すことができます。

From 446b6a4f6bb376abdd6ebee5546f568d8c5dbc83 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 16 Dec 2017 05:04:02 +0900
Subject: [PATCH 092/186] :v:

---
 .../docs/api/endpoints/posts/timeline.yaml    | 32 +++++++++++++++++++
 src/web/docs/api/endpoints/view.pug           |  7 ++--
 src/web/docs/api/gulpfile.ts                  |  4 +--
 3 files changed, 38 insertions(+), 5 deletions(-)
 create mode 100644 src/web/docs/api/endpoints/posts/timeline.yaml

diff --git a/src/web/docs/api/endpoints/posts/timeline.yaml b/src/web/docs/api/endpoints/posts/timeline.yaml
new file mode 100644
index 000000000..e1d78c082
--- /dev/null
+++ b/src/web/docs/api/endpoints/posts/timeline.yaml
@@ -0,0 +1,32 @@
+endpoint: "posts/timeline"
+
+desc:
+  ja: "タイムラインを取得します。"
+  en: "Get your timeline."
+
+params:
+  - name: "limit"
+    type: "number"
+    optional: true
+    desc:
+      ja: "取得する最大の数"
+  - name: "since_id"
+    type: "id(Post)"
+    optional: true
+    desc:
+      ja: "指定すると、この投稿を基点としてより新しい投稿を取得します"
+  - name: "max_id"
+    type: "id(Post)"
+    optional: true
+    desc:
+      ja: "指定すると、この投稿を基点としてより古い投稿を取得します"
+  - name: "since_date"
+    type: "number"
+    optional: true
+    desc:
+      ja: "指定した時間を基点としてより新しい投稿を取得します。数値は、1970 年 1 月 1 日 00:00:00 UTC から指定した日時までの経過時間をミリ秒単位で表します。"
+  - name: "max_date"
+    type: "number"
+    optional: true
+    desc:
+      ja: "指定した時間を基点としてより古い投稿を取得します。数値は、1970 年 1 月 1 日 00:00:00 UTC から指定した日時までの経過時間をミリ秒単位で表します。"
diff --git a/src/web/docs/api/endpoints/view.pug b/src/web/docs/api/endpoints/view.pug
index d456022f6..62a6f59ed 100644
--- a/src/web/docs/api/endpoints/view.pug
+++ b/src/web/docs/api/endpoints/view.pug
@@ -21,6 +21,7 @@ block main
 					h3= paramDef.name
 					+propTable(paramDef.params)
 
-	section
-		h2 Response
-		+propTable(res)
+	if res
+		section
+			h2 Response
+			+propTable(res)
diff --git a/src/web/docs/api/gulpfile.ts b/src/web/docs/api/gulpfile.ts
index 139ae9241..908280453 100644
--- a/src/web/docs/api/gulpfile.ts
+++ b/src/web/docs/api/gulpfile.ts
@@ -106,8 +106,8 @@ gulp.task('doc:api:endpoints', () => {
 				desc: ep.desc,
 				params: sortParams(ep.params.map(p => parseParam(p))),
 				paramDefs: extractDefs(ep.params),
-				res: sortParams(ep.res.map(p => parseParam(p))),
-				resDefs: extractDefs(ep.res),
+				res: ep.res ? sortParams(ep.res.map(p => parseParam(p))) : null,
+				resDefs: ep.res ? extractDefs(ep.res) : null,
 			};
 			langs.forEach(lang => {
 				pug.renderFile('./src/web/docs/api/endpoints/view.pug', Object.assign({}, vars, {

From b563a67b8a504ce13d13d4b9d9fcb3744634e205 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 16 Dec 2017 22:28:51 +0900
Subject: [PATCH 093/186] v3390

---
 CHANGELOG.md | 4 ++++
 package.json | 2 +-
 2 files changed, 5 insertions(+), 1 deletion(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9ab4698ab..a8e1fee78 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,10 @@ ChangeLog (Release Notes)
 =========================
 主に notable な changes を書いていきます
 
+3390 (2017/12/16)
+-----------------
+* ドキュメントなど
+
 3347 (2017/12/11)
 -----------------
 * バグ修正
diff --git a/package.json b/package.json
index 69090349e..29ba72bbe 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
 	"name": "misskey",
 	"author": "syuilo <i@syuilo.com>",
-	"version": "0.0.3347",
+	"version": "0.0.3390",
 	"license": "MIT",
 	"description": "A miniblog-based SNS",
 	"bugs": "https://github.com/syuilo/misskey/issues",

From 076956640871df99249e43e7df133f4f4e06043e Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 17 Dec 2017 01:41:22 +0900
Subject: [PATCH 094/186] :v:

---
 docs/setup.en.md                            |  2 +-
 docs/setup.ja.md                            |  2 +-
 gulpfile.ts                                 | 15 ++++-----------
 package.json                                |  2 --
 src/config.ts                               |  4 ++--
 src/web/app/common/tags/introduction.tag    |  2 +-
 src/web/app/common/tags/nav-links.tag       |  5 ++++-
 src/web/app/common/tags/signup.tag          |  4 +++-
 src/web/app/common/tags/twitter-setting.tag |  2 +-
 src/web/app/desktop/tags/pages/entrance.tag |  2 +-
 src/web/app/mobile/tags/ui.tag              |  4 +++-
 src/web/docs/about.en.pug                   |  3 +++
 src/web/docs/about.ja.pug                   |  3 +++
 src/web/docs/api/endpoints/view.pug         |  2 +-
 src/web/docs/api/entities/view.pug          |  2 +-
 src/web/docs/api/mixins.pug                 |  4 ++--
 src/web/docs/gulpfile.ts                    |  2 +-
 src/web/docs/layout.pug                     |  4 ++--
 src/web/docs/server.ts                      | 21 +++++++++++++++++++++
 src/web/docs/tou.ja.pug                     |  3 +++
 src/web/docs/tou.md                         |  4 ----
 src/web/server.ts                           | 11 +++++------
 tools/letsencrypt/get-cert.sh               |  2 +-
 webpack/plugins/consts.ts                   |  2 +-
 24 files changed, 65 insertions(+), 42 deletions(-)
 create mode 100644 src/web/docs/about.en.pug
 create mode 100644 src/web/docs/about.ja.pug
 create mode 100644 src/web/docs/server.ts
 create mode 100644 src/web/docs/tou.ja.pug
 delete mode 100644 src/web/docs/tou.md

diff --git a/docs/setup.en.md b/docs/setup.en.md
index b81245d89..13b0bdaeb 100644
--- a/docs/setup.en.md
+++ b/docs/setup.en.md
@@ -24,7 +24,7 @@ Note that Misskey uses following subdomains:
 
 * **api**.*{primary domain}*
 * **auth**.*{primary domain}*
-* **about**.*{primary domain}*
+* **docs**.*{primary domain}*
 * **ch**.*{primary domain}*
 * **stats**.*{primary domain}*
 * **status**.*{primary domain}*
diff --git a/docs/setup.ja.md b/docs/setup.ja.md
index 1662d1ee5..564c79097 100644
--- a/docs/setup.ja.md
+++ b/docs/setup.ja.md
@@ -25,7 +25,7 @@ Misskeyは以下のサブドメインを使います:
 
 * **api**.*{primary domain}*
 * **auth**.*{primary domain}*
-* **about**.*{primary domain}*
+* **docs**.*{primary domain}*
 * **ch**.*{primary domain}*
 * **stats**.*{primary domain}*
 * **status**.*{primary domain}*
diff --git a/gulpfile.ts b/gulpfile.ts
index e7d477061..3b7a12640 100644
--- a/gulpfile.ts
+++ b/gulpfile.ts
@@ -9,7 +9,6 @@ import * as gulp from 'gulp';
 import * as gutil from 'gulp-util';
 import * as ts from 'gulp-typescript';
 import tslint from 'gulp-tslint';
-import * as es from 'event-stream';
 import cssnano = require('gulp-cssnano');
 import * as uglifyComposer from 'gulp-uglify/composer';
 import pug = require('gulp-pug');
@@ -74,16 +73,10 @@ gulp.task('build:ts', () => {
 });
 
 gulp.task('build:copy', () =>
-	es.merge(
-		gulp.src([
-			'./src/**/assets/**/*',
-			'!./src/web/app/**/assets/**/*'
-		]).pipe(gulp.dest('./built/')) as any,
-		gulp.src([
-			'./src/web/about/**/*',
-			'!./src/web/about/**/*.pug'
-		]).pipe(gulp.dest('./built/web/about/')) as any
-	)
+	gulp.src([
+		'./src/**/assets/**/*',
+		'!./src/web/app/**/assets/**/*'
+	]).pipe(gulp.dest('./built/'))
 );
 
 gulp.task('test', ['lint', 'mocha']);
diff --git a/package.json b/package.json
index 29ba72bbe..8c0cf340d 100644
--- a/package.json
+++ b/package.json
@@ -38,7 +38,6 @@
 		"@types/debug": "0.0.30",
 		"@types/deep-equal": "1.0.1",
 		"@types/elasticsearch": "5.0.19",
-		"@types/event-stream": "3.3.33",
 		"@types/eventemitter3": "2.0.2",
 		"@types/express": "4.0.39",
 		"@types/gm": "1.17.33",
@@ -99,7 +98,6 @@
 		"diskusage": "0.2.4",
 		"elasticsearch": "14.0.0",
 		"escape-regexp": "0.0.1",
-		"event-stream": "3.3.4",
 		"eventemitter3": "3.0.0",
 		"exif-js": "2.3.0",
 		"express": "4.16.2",
diff --git a/src/config.ts b/src/config.ts
index 3ff800758..3ffefe278 100644
--- a/src/config.ts
+++ b/src/config.ts
@@ -101,7 +101,7 @@ type Mixin = {
 	secondary_scheme: string;
 	api_url: string;
 	auth_url: string;
-	about_url: string;
+	docs_url: string;
 	ch_url: string;
 	stats_url: string;
 	status_url: string;
@@ -131,7 +131,7 @@ export default function load() {
 	mixin.auth_url = `${mixin.scheme}://auth.${mixin.host}`;
 	mixin.ch_url = `${mixin.scheme}://ch.${mixin.host}`;
 	mixin.dev_url = `${mixin.scheme}://dev.${mixin.host}`;
-	mixin.about_url = `${mixin.scheme}://about.${mixin.host}`;
+	mixin.docs_url = `${mixin.scheme}://docs.${mixin.host}`;
 	mixin.stats_url = `${mixin.scheme}://stats.${mixin.host}`;
 	mixin.status_url = `${mixin.scheme}://status.${mixin.host}`;
 	mixin.drive_url = `${mixin.secondary_scheme}://file.${mixin.secondary_host}`;
diff --git a/src/web/app/common/tags/introduction.tag b/src/web/app/common/tags/introduction.tag
index 3256688d1..28afc6fa4 100644
--- a/src/web/app/common/tags/introduction.tag
+++ b/src/web/app/common/tags/introduction.tag
@@ -3,7 +3,7 @@
 		<h1>Misskeyとは?</h1>
 		<p><ruby>Misskey<rt>みすきー</rt></ruby>は、<a href="http://syuilo.com" target="_blank">syuilo</a>が2014年くらいから<a href="https://github.com/syuilo/misskey" target="_blank">オープンソースで</a>開発・運営を行っている、ミニブログベースのSNSです。</p>
 		<p>無料で誰でも利用でき、広告も掲載していません。</p>
-		<p><a href={ _ABOUT_URL_ } target="_blank">もっと知りたい方はこちら</a></p>
+		<p><a href={ _DOCS_URL_ } target="_blank">もっと知りたい方はこちら</a></p>
 	</article>
 	<style>
 		:scope
diff --git a/src/web/app/common/tags/nav-links.tag b/src/web/app/common/tags/nav-links.tag
index 71f0453db..ea122575a 100644
--- a/src/web/app/common/tags/nav-links.tag
+++ b/src/web/app/common/tags/nav-links.tag
@@ -1,7 +1,10 @@
 <mk-nav-links>
-	<a href={ _ABOUT_URL_ }>%i18n:common.tags.mk-nav-links.about%</a><i>・</i><a href={ _STATS_URL_ }>%i18n:common.tags.mk-nav-links.stats%</a><i>・</i><a href={ _STATUS_URL_ }>%i18n:common.tags.mk-nav-links.status%</a><i>・</i><a href="http://zawazawa.jp/misskey/">%i18n:common.tags.mk-nav-links.wiki%</a><i>・</i><a href="https://github.com/syuilo/misskey/blob/master/DONORS.md">%i18n:common.tags.mk-nav-links.donors%</a><i>・</i><a href="https://github.com/syuilo/misskey">%i18n:common.tags.mk-nav-links.repository%</a><i>・</i><a href={ _DEV_URL_ }>%i18n:common.tags.mk-nav-links.develop%</a><i>・</i><a href="https://twitter.com/misskey_xyz" target="_blank">Follow us on %fa:B twitter%</a>
+	<a href={ aboutUrl }>%i18n:common.tags.mk-nav-links.about%</a><i>・</i><a href={ _STATS_URL_ }>%i18n:common.tags.mk-nav-links.stats%</a><i>・</i><a href={ _STATUS_URL_ }>%i18n:common.tags.mk-nav-links.status%</a><i>・</i><a href="http://zawazawa.jp/misskey/">%i18n:common.tags.mk-nav-links.wiki%</a><i>・</i><a href="https://github.com/syuilo/misskey/blob/master/DONORS.md">%i18n:common.tags.mk-nav-links.donors%</a><i>・</i><a href="https://github.com/syuilo/misskey">%i18n:common.tags.mk-nav-links.repository%</a><i>・</i><a href={ _DEV_URL_ }>%i18n:common.tags.mk-nav-links.develop%</a><i>・</i><a href="https://twitter.com/misskey_xyz" target="_blank">Follow us on %fa:B twitter%</a>
 	<style>
 		:scope
 			display inline
 	</style>
+	<script>
+		this.aboutUrl = `${_DOCS_URL_}/${_LANG_}/about`;
+	</script>
 </mk-nav-links>
diff --git a/src/web/app/common/tags/signup.tag b/src/web/app/common/tags/signup.tag
index 4816fe66d..b488efb92 100644
--- a/src/web/app/common/tags/signup.tag
+++ b/src/web/app/common/tags/signup.tag
@@ -34,7 +34,7 @@
 		</label>
 		<label class="agree-tou">
 			<input name="agree-tou" type="checkbox" autocomplete="off" required="required"/>
-			<p><a href="https://github.com/syuilo/misskey/blob/master/src/docs/tou.md" target="_blank">利用規約</a>に同意する</p>
+			<p><a href={ touUrl } target="_blank">利用規約</a>に同意する</p>
 		</label>
 		<button onclick={ onsubmit }>%i18n:common.tags.mk-signup.create%</button>
 	</form>
@@ -182,6 +182,8 @@
 		this.passwordRetypeState = null;
 		this.recaptchaed = false;
 
+		this.aboutUrl = `${_DOCS_URL_}/${_LANG_}/tou`;
+
 		window.onRecaptchaed = () => {
 			this.recaptchaed = true;
 			this.update();
diff --git a/src/web/app/common/tags/twitter-setting.tag b/src/web/app/common/tags/twitter-setting.tag
index 3b70505ba..4d57cfa55 100644
--- a/src/web/app/common/tags/twitter-setting.tag
+++ b/src/web/app/common/tags/twitter-setting.tag
@@ -1,5 +1,5 @@
 <mk-twitter-setting>
-	<p>%i18n:common.tags.mk-twitter-setting.description%<a href={ _ABOUT_URL_ + '/link-to-twitter' } target="_blank">%i18n:common.tags.mk-twitter-setting.detail%</a></p>
+	<p>%i18n:common.tags.mk-twitter-setting.description%<a href={ _DOCS_URL_ + '/link-to-twitter' } target="_blank">%i18n:common.tags.mk-twitter-setting.detail%</a></p>
 	<p class="account" if={ I.twitter } title={ 'Twitter ID: ' + I.twitter.user_id }>%i18n:common.tags.mk-twitter-setting.connected-to%: <a href={ 'https://twitter.com/' + I.twitter.screen_name } target="_blank">@{ I.twitter.screen_name }</a></p>
 	<p>
 		<a href={ _API_URL_ + '/connect/twitter' } target="_blank" onclick={ connect }>{ I.twitter ? '%i18n:common.tags.mk-twitter-setting.reconnect%' : '%i18n:common.tags.mk-twitter-setting.connect%' }</a>
diff --git a/src/web/app/desktop/tags/pages/entrance.tag b/src/web/app/desktop/tags/pages/entrance.tag
index 44548e418..b07b22c80 100644
--- a/src/web/app/desktop/tags/pages/entrance.tag
+++ b/src/web/app/desktop/tags/pages/entrance.tag
@@ -150,7 +150,7 @@
 </mk-entrance>
 
 <mk-entrance-signin>
-	<a class="help" href={ _ABOUT_URL_ + '/help' } title="お困りですか?">%fa:question%</a>
+	<a class="help" href={ _DOCS_URL_ + '/help' } title="お困りですか?">%fa:question%</a>
 	<div class="form">
 		<h1><img if={ user } src={ user.avatar_url + '?thumbnail&size=32' }/>
 			<p>{ user ? user.name : 'アカウント' }</p>
diff --git a/src/web/app/mobile/tags/ui.tag b/src/web/app/mobile/tags/ui.tag
index 62e128489..621f89f33 100644
--- a/src/web/app/mobile/tags/ui.tag
+++ b/src/web/app/mobile/tags/ui.tag
@@ -248,7 +248,7 @@
 				<li><a href="/i/settings">%fa:cog%%i18n:mobile.tags.mk-ui-nav.settings%%fa:angle-right%</a></li>
 			</ul>
 		</div>
-		<a href={ _ABOUT_URL_ }><p class="about">%i18n:mobile.tags.mk-ui-nav.about%</p></a>
+		<a href={ aboutUrl }><p class="about">%i18n:mobile.tags.mk-ui-nav.about%</p></a>
 	</div>
 	<style>
 		:scope
@@ -359,6 +359,8 @@
 		this.connection = this.stream.getConnection();
 		this.connectionId = this.stream.use();
 
+		this.aboutUrl = `${_DOCS_URL_}/${_LANG_}/about`;
+
 		this.on('mount', () => {
 			this.connection.on('read_all_notifications', this.onReadAllNotifications);
 			this.connection.on('read_all_messaging_messages', this.onReadAllMessagingMessages);
diff --git a/src/web/docs/about.en.pug b/src/web/docs/about.en.pug
new file mode 100644
index 000000000..893d9dd6a
--- /dev/null
+++ b/src/web/docs/about.en.pug
@@ -0,0 +1,3 @@
+h1 About Misskey
+
+p Misskey is a mini blog SNS.
diff --git a/src/web/docs/about.ja.pug b/src/web/docs/about.ja.pug
new file mode 100644
index 000000000..fec933b0c
--- /dev/null
+++ b/src/web/docs/about.ja.pug
@@ -0,0 +1,3 @@
+h1 Misskeyについて
+
+p MisskeyはミニブログSNSです。
diff --git a/src/web/docs/api/endpoints/view.pug b/src/web/docs/api/endpoints/view.pug
index 62a6f59ed..9ba1c4e85 100644
--- a/src/web/docs/api/endpoints/view.pug
+++ b/src/web/docs/api/endpoints/view.pug
@@ -2,7 +2,7 @@ extends ../../layout.pug
 include ../mixins
 
 block meta
-	link(rel="stylesheet" href="/assets/docs/api/endpoints/style.css")
+	link(rel="stylesheet" href="/assets/api/endpoints/style.css")
 
 block main
 	h1= endpoint
diff --git a/src/web/docs/api/entities/view.pug b/src/web/docs/api/entities/view.pug
index 57c6d4cad..6fc05bd55 100644
--- a/src/web/docs/api/entities/view.pug
+++ b/src/web/docs/api/entities/view.pug
@@ -2,7 +2,7 @@ extends ../../layout.pug
 include ../mixins
 
 block meta
-	link(rel="stylesheet" href="/assets/docs/api/entities/style.css")
+	link(rel="stylesheet" href="/assets/api/entities/style.css")
 
 block main
 	h1= name
diff --git a/src/web/docs/api/mixins.pug b/src/web/docs/api/mixins.pug
index 3ddd7cb48..518069857 100644
--- a/src/web/docs/api/mixins.pug
+++ b/src/web/docs/api/mixins.pug
@@ -14,13 +14,13 @@ mixin propTable(props)
 						if prop.kind == 'id'
 							if prop.entity
 								|  (
-								a(href=`/docs/${lang}/api/entities/${kebab(prop.entity)}`)= prop.entity
+								a(href=`/${lang}/api/entities/${kebab(prop.entity)}`)= prop.entity
 								|  ID)
 							else
 								|  (ID)
 						else if prop.kind == 'entity'
 							|   (
-							a(href=`/docs/${lang}/api/entities/${kebab(prop.entity)}`)= prop.entity
+							a(href=`/${lang}/api/entities/${kebab(prop.entity)}`)= prop.entity
 							| )
 						else if prop.kind == 'object'
 							if prop.def
diff --git a/src/web/docs/gulpfile.ts b/src/web/docs/gulpfile.ts
index 237784465..61e44a1dc 100644
--- a/src/web/docs/gulpfile.ts
+++ b/src/web/docs/gulpfile.ts
@@ -70,5 +70,5 @@ gulp.task('doc:styles', () =>
 	gulp.src('./src/web/docs/**/*.styl')
 		.pipe(stylus())
 		.pipe((cssnano as any)())
-		.pipe(gulp.dest('./built/web/assets/docs/'))
+		.pipe(gulp.dest('./built/web/docs/assets/'))
 );
diff --git a/src/web/docs/layout.pug b/src/web/docs/layout.pug
index ac3743d2f..bc9710d7c 100644
--- a/src/web/docs/layout.pug
+++ b/src/web/docs/layout.pug
@@ -6,9 +6,9 @@ html(lang= lang)
 		meta(name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1, user-scalable=no")
 		title
 			| #{title} | Misskey Docs
-		link(rel="stylesheet" href="/assets/docs/style.css")
+		link(rel="stylesheet" href="/assets/style.css")
 		block meta
-		base(href=`/docs/${lang}/`)
+		base(href=`/${lang}/`)
 
 	body
 		nav
diff --git a/src/web/docs/server.ts b/src/web/docs/server.ts
new file mode 100644
index 000000000..b2e50457e
--- /dev/null
+++ b/src/web/docs/server.ts
@@ -0,0 +1,21 @@
+/**
+ * Docs Server
+ */
+
+import * as express from 'express';
+
+/**
+ * Init app
+ */
+const app = express();
+app.disable('x-powered-by');
+
+app.use('/assets', express.static(`${__dirname}/assets`));
+
+/**
+ * Routing
+ */
+app.get(/^\/([a-z_\-\/]+?)$/, (req, res) =>
+	res.sendFile(`${__dirname}/${req.params[0]}.html`));
+
+module.exports = app;
diff --git a/src/web/docs/tou.ja.pug b/src/web/docs/tou.ja.pug
new file mode 100644
index 000000000..7663258f8
--- /dev/null
+++ b/src/web/docs/tou.ja.pug
@@ -0,0 +1,3 @@
+h1 利用規約
+
+p 公序良俗に反する行為はおやめください。
diff --git a/src/web/docs/tou.md b/src/web/docs/tou.md
deleted file mode 100644
index fbf87867b..000000000
--- a/src/web/docs/tou.md
+++ /dev/null
@@ -1,4 +0,0 @@
-利用規約
-================================================================
-
-公序良俗に反する行為はおやめください。
diff --git a/src/web/server.ts b/src/web/server.ts
index 38e87754f..062d1f197 100644
--- a/src/web/server.ts
+++ b/src/web/server.ts
@@ -10,6 +10,9 @@ import * as express from 'express';
 import * as bodyParser from 'body-parser';
 import * as favicon from 'serve-favicon';
 import * as compression from 'compression';
+import vhost = require('vhost');
+
+import config from '../conf';
 
 /**
  * Init app
@@ -17,6 +20,8 @@ import * as compression from 'compression';
 const app = express();
 app.disable('x-powered-by');
 
+app.use(vhost(`docs.${config.host}`, require('./docs/server')));
+
 app.use(bodyParser.urlencoded({ extended: true }));
 app.use(bodyParser.json({
 	type: ['application/json', 'text/plain']
@@ -63,12 +68,6 @@ app.get('/manifest.json', (req, res) =>
  */
 app.get(/\/api:url/, require('./service/url-preview'));
 
-/**
- * Docs
- */
-app.get(/^\/docs\/([a-z_\-\/]+?)$/, (req, res) =>
-	res.sendFile(`${__dirname}/docs/${req.params[0]}.html`));
-
 /**
  * Routing
  */
diff --git a/tools/letsencrypt/get-cert.sh b/tools/letsencrypt/get-cert.sh
index 409f2fa5e..d44deb144 100644
--- a/tools/letsencrypt/get-cert.sh
+++ b/tools/letsencrypt/get-cert.sh
@@ -4,7 +4,7 @@ certbot certonly --standalone\
   -d $1\
   -d api.$1\
   -d auth.$1\
-  -d about.$1\
+  -d docs.$1\
   -d ch.$1\
   -d stats.$1\
   -d status.$1\
diff --git a/webpack/plugins/consts.ts b/webpack/plugins/consts.ts
index 7d1ff7c8d..6e18fa296 100644
--- a/webpack/plugins/consts.ts
+++ b/webpack/plugins/consts.ts
@@ -16,7 +16,7 @@ export default lang => {
 		_VERSION_: version,
 		_STATUS_URL_: config.status_url,
 		_STATS_URL_: config.stats_url,
-		_ABOUT_URL_: config.about_url,
+		_DOCS_URL_: config.docs_url,
 		_API_URL_: config.api_url,
 		_DEV_URL_: config.dev_url,
 		_CH_URL_: config.ch_url,

From 5684cf8fd3f81e336fa015001f710f3d32a6f1c8 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 17 Dec 2017 02:56:53 +0900
Subject: [PATCH 095/186] v3392

---
 CHANGELOG.md | 4 ++++
 package.json | 2 +-
 2 files changed, 5 insertions(+), 1 deletion(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index a8e1fee78..f1f0c8138 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,10 @@ ChangeLog (Release Notes)
 =========================
 主に notable な changes を書いていきます
 
+3392 (2017/12/17)
+-----------------
+* ドキュメントなど
+
 3390 (2017/12/16)
 -----------------
 * ドキュメントなど
diff --git a/package.json b/package.json
index 8c0cf340d..6e7bf7ef5 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
 	"name": "misskey",
 	"author": "syuilo <i@syuilo.com>",
-	"version": "0.0.3390",
+	"version": "0.0.3392",
 	"license": "MIT",
 	"description": "A miniblog-based SNS",
 	"bugs": "https://github.com/syuilo/misskey/issues",

From da279f9e50839da68746ad6460045d0f358818ae Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 17 Dec 2017 04:02:30 +0900
Subject: [PATCH 096/186] :v:

---
 locales/en.yml                        | 18 ++++++++++++++++++
 webpack/langs.ts => locales/index.ts  |  6 +++---
 locales/ja.yml                        | 18 ++++++++++++++++++
 src/web/docs/api/endpoints/style.styl | 15 ++++++++++++++-
 src/web/docs/api/endpoints/view.pug   | 11 ++++++++---
 src/web/docs/api/entities/view.pug    |  2 +-
 src/web/docs/api/gulpfile.ts          |  9 +++++++--
 src/web/docs/api/mixins.pug           | 14 +++++++++-----
 src/web/docs/gulpfile.ts              |  3 ++-
 src/web/docs/layout.pug               | 12 +++++++++---
 src/web/docs/style.styl               | 17 ++++++++---------
 src/web/docs/vars.ts                  |  7 +++++--
 webpack/webpack.config.ts             |  4 ++--
 13 files changed, 104 insertions(+), 32 deletions(-)
 rename webpack/langs.ts => locales/index.ts (85%)

diff --git a/locales/en.yml b/locales/en.yml
index b49af68bd..57e0c4116 100644
--- a/locales/en.yml
+++ b/locales/en.yml
@@ -183,6 +183,24 @@ common:
     mk-uploader:
       waiting: "Waiting"
 
+docs:
+  edit-this-page-on-github: "Caught a mistake or want to contribute to the documentation? "
+  edit-this-page-on-github-link: "Edit this page on Github!"
+
+  api:
+    entities:
+      properties: "Properties"
+    endpoints:
+      params: "Parameters"
+      res: "Response"
+    props:
+      name: "Name"
+      type: "Type"
+      optional: "Optional"
+      description: "Description"
+      yes: "Yes"
+      no: "No"
+
 ch:
   tags:
     mk-index:
diff --git a/webpack/langs.ts b/locales/index.ts
similarity index 85%
rename from webpack/langs.ts
rename to locales/index.ts
index 409b25504..0593af366 100644
--- a/webpack/langs.ts
+++ b/locales/index.ts
@@ -10,12 +10,12 @@ const loadLang = lang => yaml.safeLoad(
 
 const native = loadLang('ja');
 
-const langs = Object.entries({
+const langs = {
 	'en': loadLang('en'),
 	'ja': native
-});
+};
 
-langs.map(([, locale]) => {
+Object.entries(langs).map(([, locale]) => {
 	// Extend native language (Japanese)
 	locale = Object.assign({}, native, locale);
 });
diff --git a/locales/ja.yml b/locales/ja.yml
index afafa5a63..ee52f0716 100644
--- a/locales/ja.yml
+++ b/locales/ja.yml
@@ -183,6 +183,24 @@ common:
     mk-uploader:
       waiting: "待機中"
 
+docs:
+  edit-this-page-on-github: "間違いや改善点を見つけましたか?"
+  edit-this-page-on-github-link: "このページをGitHubで編集"
+
+  api:
+    entities:
+      properties: "プロパティ"
+    endpoints:
+      params: "パラメータ"
+      res: "レスポンス"
+    props:
+      name: "名前"
+      type: "型"
+      optional: "オプション"
+      description: "説明"
+      yes: "はい"
+      no: "いいえ"
+
 ch:
   tags:
     mk-index:
diff --git a/src/web/docs/api/endpoints/style.styl b/src/web/docs/api/endpoints/style.styl
index 07fb7ec2a..2af9fe9a7 100644
--- a/src/web/docs/api/endpoints/style.styl
+++ b/src/web/docs/api/endpoints/style.styl
@@ -1,8 +1,21 @@
 @import "../style"
 
 #url
-	padding 8px 12px
+	padding 8px 12px 8px 8px
 	font-family Consolas, 'Courier New', Courier, Monaco, monospace
 	color #fff
 	background #222e40
 	border-radius 4px
+
+	> .method
+		display inline-block
+		margin 0 8px 0 0
+		padding 0 6px
+		color #f4fcff
+		background #17afc7
+		border-radius 4px
+		user-select none
+		pointer-events none
+
+	> .host
+		opacity 0.7
diff --git a/src/web/docs/api/endpoints/view.pug b/src/web/docs/api/endpoints/view.pug
index 9ba1c4e85..90084ab27 100644
--- a/src/web/docs/api/endpoints/view.pug
+++ b/src/web/docs/api/endpoints/view.pug
@@ -7,12 +7,17 @@ block meta
 block main
 	h1= endpoint
 
-	p#url= url
+	p#url
+		span.method POST
+		span.host
+			= url.host
+			| /
+		span.path= url.path
 
 	p#desc= desc[lang] || desc['ja']
 
 	section
-		h2 Params
+		h2= common.i18n[lang]['docs']['api']['endpoints']['params']
 		+propTable(params)
 
 		if paramDefs
@@ -23,5 +28,5 @@ block main
 
 	if res
 		section
-			h2 Response
+			h2= common.i18n[lang]['docs']['api']['endpoints']['res']
 			+propTable(res)
diff --git a/src/web/docs/api/entities/view.pug b/src/web/docs/api/entities/view.pug
index 6fc05bd55..99e786c69 100644
--- a/src/web/docs/api/entities/view.pug
+++ b/src/web/docs/api/entities/view.pug
@@ -10,7 +10,7 @@ block main
 	p#desc= desc[lang] || desc['ja']
 
 	section
-		h2 Properties
+		h2= common.i18n[lang]['docs']['api']['entities']['properties']
 		+propTable(props)
 
 		if propDefs
diff --git a/src/web/docs/api/gulpfile.ts b/src/web/docs/api/gulpfile.ts
index 908280453..2e8409c59 100644
--- a/src/web/docs/api/gulpfile.ts
+++ b/src/web/docs/api/gulpfile.ts
@@ -16,7 +16,7 @@ import generateVars from '../vars';
 
 const commonVars = generateVars();
 
-const langs = ['ja', 'en'];
+const langs = Object.keys(commonVars.i18n);
 
 const kebab = string => string.replace(/([a-z])([A-Z])/g, '$1-$2').replace(/\s+/g, '-').toLowerCase();
 
@@ -102,7 +102,10 @@ gulp.task('doc:api:endpoints', () => {
 			const ep = yaml.safeLoad(fs.readFileSync(file, 'utf-8'));
 			const vars = {
 				endpoint: ep.endpoint,
-				url: `${config.api_url}/${ep.endpoint}`,
+				url: {
+					host: config.api_url,
+					path: ep.endpoint
+				},
 				desc: ep.desc,
 				params: sortParams(ep.params.map(p => parseParam(p))),
 				paramDefs: extractDefs(ep.params),
@@ -113,6 +116,7 @@ gulp.task('doc:api:endpoints', () => {
 				pug.renderFile('./src/web/docs/api/endpoints/view.pug', Object.assign({}, vars, {
 					lang,
 					title: ep.endpoint,
+					src: `https://github.com/syuilo/misskey/tree/master/src/web/docs/api/endpoints/${ep.endpoint}.yaml`,
 					kebab,
 					common: commonVars
 				}), (renderErr, html) => {
@@ -152,6 +156,7 @@ gulp.task('doc:api:entities', () => {
 				pug.renderFile('./src/web/docs/api/entities/view.pug', Object.assign({}, vars, {
 					lang,
 					title: entity.name,
+					src: `https://github.com/syuilo/misskey/tree/master/src/web/docs/api/entities/${kebab(entity.name)}.yaml`,
 					kebab,
 					common: commonVars
 				}), (renderErr, html) => {
diff --git a/src/web/docs/api/mixins.pug b/src/web/docs/api/mixins.pug
index 518069857..b563a121d 100644
--- a/src/web/docs/api/mixins.pug
+++ b/src/web/docs/api/mixins.pug
@@ -1,10 +1,10 @@
 mixin propTable(props)
 	table.props
 		thead: tr
-			th Name
-			th Type
-			th Optional
-			th Description
+			th= common.i18n[lang]['docs']['api']['props']['name']
+			th= common.i18n[lang]['docs']['api']['props']['type']
+			th= common.i18n[lang]['docs']['api']['props']['optional']
+			th= common.i18n[lang]['docs']['api']['props']['description']
 		tbody
 			each prop in props
 				tr
@@ -29,5 +29,9 @@ mixin propTable(props)
 								| )
 						else if prop.kind == 'date'
 							|  (Date)
-					td.optional= prop.optional.toString()
+					td.optional
+						if prop.optional
+							= common.i18n[lang]['docs']['api']['props']['yes']
+						else
+							= common.i18n[lang]['docs']['api']['props']['no']
 					td.desc!= prop.desc[lang] || prop.desc['ja']
diff --git a/src/web/docs/gulpfile.ts b/src/web/docs/gulpfile.ts
index 61e44a1dc..6668abdec 100644
--- a/src/web/docs/gulpfile.ts
+++ b/src/web/docs/gulpfile.ts
@@ -37,7 +37,8 @@ gulp.task('doc:docs', () => {
 			const vars = {
 				common: commonVars,
 				lang: lang,
-				title: fs.readFileSync(file, 'utf-8').match(/^h1 (.+?)\r?\n/)[1]
+				title: fs.readFileSync(file, 'utf-8').match(/^h1 (.+?)\r?\n/)[1],
+				src: `https://github.com/syuilo/misskey/tree/master/src/web/docs/${name}.${lang}.pug`,
 			};
 			pug.renderFile(file, vars, (renderErr, content) => {
 				if (renderErr) {
diff --git a/src/web/docs/layout.pug b/src/web/docs/layout.pug
index bc9710d7c..c37967ab8 100644
--- a/src/web/docs/layout.pug
+++ b/src/web/docs/layout.pug
@@ -27,6 +27,12 @@ html(lang= lang)
 							each endpoint in common.endpoints
 								li: a(href=`./api/endpoints/${common.kebab(endpoint)}`)= endpoint
 		main
-			block main
-			if content
-				| !{content}
+			article
+				block main
+				if content
+					| !{content}
+
+			footer
+				p
+					= common.i18n[lang]['docs']['edit-this-page-on-github']
+					a(href=src target="_blank")= common.i18n[lang]['docs']['edit-this-page-on-github-link']
diff --git a/src/web/docs/style.styl b/src/web/docs/style.styl
index f222e65bf..285b92bdb 100644
--- a/src/web/docs/style.styl
+++ b/src/web/docs/style.styl
@@ -37,6 +37,14 @@ main
 		margin 1em 0
 		line-height 1.6em
 
+	footer
+		margin 32px 0 0 0
+		border-top solid 2px #eee
+
+		.copyright
+			margin 16px 0 0 0
+			color #aaa
+
 nav
 	display block
 	position fixed
@@ -48,15 +56,6 @@ nav
 	padding 32px
 	border-right solid 2px #eee
 
-footer
-	padding:32px 0 0 0
-	margin 32px 0 0 0
-	border-top solid 1px #eee
-
-	.copyright
-		margin 16px 0 0 0
-		color #aaa
-
 table
 	width 100%
 	border-spacing 0
diff --git a/src/web/docs/vars.ts b/src/web/docs/vars.ts
index ffa262a06..2c744be61 100644
--- a/src/web/docs/vars.ts
+++ b/src/web/docs/vars.ts
@@ -1,10 +1,11 @@
 import * as fs from 'fs';
 import * as glob from 'glob';
 import * as yaml from 'js-yaml';
+import langs from '../../../locales';
 import config from '../../conf';
 
-export default function() {
-	const vars = {};
+export default function(): { [key: string]: any } {
+	const vars = {} as { [key: string]: any };
 
 	const endpoints = glob.sync('./src/web/docs/api/endpoints/**/*.yaml');
 	vars['endpoints'] = endpoints.map(ep => {
@@ -35,5 +36,7 @@ export default function() {
 
 	vars['config'] = config;
 
+	vars['i18n'] = langs;
+
 	return vars;
 }
diff --git a/webpack/webpack.config.ts b/webpack/webpack.config.ts
index 753d89fed..124bd975b 100644
--- a/webpack/webpack.config.ts
+++ b/webpack/webpack.config.ts
@@ -5,10 +5,10 @@
 import module_ from './module';
 import plugins from './plugins';
 
-import langs from './langs';
+import langs from '../locales';
 import version from '../src/version';
 
-module.exports = langs.map(([lang, locale]) => {
+module.exports = Object.entries(langs).map(([lang, locale]) => {
 	// Chunk name
 	const name = lang;
 

From 11842366bcfdaaf0de4dbdfc3b31874c4223c9e3 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 17 Dec 2017 04:05:33 +0900
Subject: [PATCH 097/186] :v:

---
 src/web/docs/layout.pug | 7 +++----
 1 file changed, 3 insertions(+), 4 deletions(-)

diff --git a/src/web/docs/layout.pug b/src/web/docs/layout.pug
index c37967ab8..22d89375a 100644
--- a/src/web/docs/layout.pug
+++ b/src/web/docs/layout.pug
@@ -8,24 +8,23 @@ html(lang= lang)
 			| #{title} | Misskey Docs
 		link(rel="stylesheet" href="/assets/style.css")
 		block meta
-		base(href=`/${lang}/`)
 
 	body
 		nav
 			ul
 				each doc in common.docs
-					li: a(href=`./${doc.name}`)= doc.title[lang] || doc.title['ja']
+					li: a(href=`/${lang}/${doc.name}`)= doc.title[lang] || doc.title['ja']
 			section
 				h2 API
 				ul
 					li Entities
 						ul
 							each entity in common.entities
-								li: a(href=`./api/entities/${common.kebab(entity)}`)= entity
+								li: a(href=`/${lang}/api/entities/${common.kebab(entity)}`)= entity
 					li Endpoints
 						ul
 							each endpoint in common.endpoints
-								li: a(href=`./api/endpoints/${common.kebab(endpoint)}`)= endpoint
+								li: a(href=`/${lang}/api/endpoints/${common.kebab(endpoint)}`)= endpoint
 		main
 			article
 				block main

From 74f3a6aadbb01b7032d40b82cf725ec706b103a3 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 17 Dec 2017 04:31:24 +0900
Subject: [PATCH 098/186] :v:

---
 src/const.json                              | 1 +
 src/web/app/common/tags/copyright.tag       | 7 -------
 src/web/app/common/tags/index.ts            | 1 -
 src/web/app/desktop/tags/pages/entrance.tag | 4 ++--
 src/web/app/mobile/tags/page/entrance.tag   | 4 ++--
 src/web/docs/layout.pug                     | 1 +
 src/web/docs/style.styl                     | 2 +-
 src/web/docs/vars.ts                        | 3 +++
 webpack/plugins/consts.ts                   | 1 +
 9 files changed, 11 insertions(+), 13 deletions(-)
 delete mode 100644 src/web/app/common/tags/copyright.tag

diff --git a/src/const.json b/src/const.json
index 924b4dd8b..0ee6ac206 100644
--- a/src/const.json
+++ b/src/const.json
@@ -1,4 +1,5 @@
 {
+	"copyright": "Copyright (c) 2014-2017 syuilo",
 	"themeColor": "#ff4e45",
 	"themeColorForeground": "#fff"
 }
diff --git a/src/web/app/common/tags/copyright.tag b/src/web/app/common/tags/copyright.tag
deleted file mode 100644
index 9c3f1f648..000000000
--- a/src/web/app/common/tags/copyright.tag
+++ /dev/null
@@ -1,7 +0,0 @@
-<mk-copyright>
-	<span>(c) syuilo 2014-2017</span>
-	<style>
-		:scope
-			display block
-	</style>
-</mk-copyright>
diff --git a/src/web/app/common/tags/index.ts b/src/web/app/common/tags/index.ts
index 2f4e1181d..df99d93cc 100644
--- a/src/web/app/common/tags/index.ts
+++ b/src/web/app/common/tags/index.ts
@@ -12,7 +12,6 @@ require('./signin.tag');
 require('./signup.tag');
 require('./forkit.tag');
 require('./introduction.tag');
-require('./copyright.tag');
 require('./signin-history.tag');
 require('./twitter-setting.tag');
 require('./authorized-apps.tag');
diff --git a/src/web/app/desktop/tags/pages/entrance.tag b/src/web/app/desktop/tags/pages/entrance.tag
index b07b22c80..974f49a4f 100644
--- a/src/web/app/desktop/tags/pages/entrance.tag
+++ b/src/web/app/desktop/tags/pages/entrance.tag
@@ -18,7 +18,7 @@
 	<footer>
 		<div>
 			<mk-nav-links/>
-			<mk-copyright/>
+			<p class="c">{ _COPYRIGHT_ }</p>
 		</div>
 	</footer>
 	<!-- ↓ https://github.com/riot/riot/issues/2134 (将来的)-->
@@ -101,7 +101,7 @@
 					text-align center
 					border-top solid 1px #fff
 
-					> mk-copyright
+					> .c
 						margin 0
 						line-height 64px
 						font-size 10px
diff --git a/src/web/app/mobile/tags/page/entrance.tag b/src/web/app/mobile/tags/page/entrance.tag
index 380fb780b..191874caf 100644
--- a/src/web/app/mobile/tags/page/entrance.tag
+++ b/src/web/app/mobile/tags/page/entrance.tag
@@ -8,7 +8,7 @@
 		</div>
 	</main>
 	<footer>
-		<mk-copyright/>
+		<p class="c">{ _COPYRIGHT_ }</p>
 	</footer>
 	<style>
 		:scope
@@ -34,7 +34,7 @@
 						margin 16px auto 0 auto
 
 			> footer
-				> mk-copyright
+				> .c
 					margin 0
 					text-align center
 					line-height 64px
diff --git a/src/web/docs/layout.pug b/src/web/docs/layout.pug
index 22d89375a..ee8018ec6 100644
--- a/src/web/docs/layout.pug
+++ b/src/web/docs/layout.pug
@@ -35,3 +35,4 @@ html(lang= lang)
 				p
 					= common.i18n[lang]['docs']['edit-this-page-on-github']
 					a(href=src target="_blank")= common.i18n[lang]['docs']['edit-this-page-on-github-link']
+				small= common.copyright
diff --git a/src/web/docs/style.styl b/src/web/docs/style.styl
index 285b92bdb..32a2264f1 100644
--- a/src/web/docs/style.styl
+++ b/src/web/docs/style.styl
@@ -41,7 +41,7 @@ main
 		margin 32px 0 0 0
 		border-top solid 2px #eee
 
-		.copyright
+		> small
 			margin 16px 0 0 0
 			color #aaa
 
diff --git a/src/web/docs/vars.ts b/src/web/docs/vars.ts
index 2c744be61..da590d7bd 100644
--- a/src/web/docs/vars.ts
+++ b/src/web/docs/vars.ts
@@ -3,6 +3,7 @@ import * as glob from 'glob';
 import * as yaml from 'js-yaml';
 import langs from '../../../locales';
 import config from '../../conf';
+const constants = require('../../const.json');
 
 export default function(): { [key: string]: any } {
 	const vars = {} as { [key: string]: any };
@@ -38,5 +39,7 @@ export default function(): { [key: string]: any } {
 
 	vars['i18n'] = langs;
 
+	vars['copyright'] = constants.copyright;
+
 	return vars;
 }
diff --git a/webpack/plugins/consts.ts b/webpack/plugins/consts.ts
index 6e18fa296..16a569162 100644
--- a/webpack/plugins/consts.ts
+++ b/webpack/plugins/consts.ts
@@ -13,6 +13,7 @@ export default lang => {
 		_RECAPTCHA_SITEKEY_: config.recaptcha.site_key,
 		_SW_PUBLICKEY_: config.sw ? config.sw.public_key : null,
 		_THEME_COLOR_: constants.themeColor,
+		_COPYRIGHT_: constants.copyright,
 		_VERSION_: version,
 		_STATUS_URL_: config.status_url,
 		_STATS_URL_: config.stats_url,

From 458329c4c024f500f6e97e73d5c907d3c76ce60c Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 17 Dec 2017 04:32:29 +0900
Subject: [PATCH 099/186] v3400

---
 CHANGELOG.md | 4 ++++
 package.json | 2 +-
 2 files changed, 5 insertions(+), 1 deletion(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index f1f0c8138..d3be42879 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,10 @@ ChangeLog (Release Notes)
 =========================
 主に notable な changes を書いていきます
 
+3400 (2017/12/17)
+-----------------
+* なんか
+
 3392 (2017/12/17)
 -----------------
 * ドキュメントなど
diff --git a/package.json b/package.json
index 01d68c206..181c20a03 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
 	"name": "misskey",
 	"author": "syuilo <i@syuilo.com>",
-	"version": "0.0.3392",
+	"version": "0.0.3400",
 	"license": "MIT",
 	"description": "A miniblog-based SNS",
 	"bugs": "https://github.com/syuilo/misskey/issues",

From 5bf9e1d23cecac2c15b74877fc3fd44093281da8 Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Sun, 17 Dec 2017 07:55:57 +0900
Subject: [PATCH 100/186] Update api.ja.pug

---
 src/web/docs/api.ja.pug | 47 +++++++++++++++++++++++++++++++++++------
 1 file changed, 40 insertions(+), 7 deletions(-)

diff --git a/src/web/docs/api.ja.pug b/src/web/docs/api.ja.pug
index 588b926bd..dbe17fe66 100644
--- a/src/web/docs/api.ja.pug
+++ b/src/web/docs/api.ja.pug
@@ -1,25 +1,54 @@
 h1 Misskey API
 
-p MisskeyはWeb APIを公開しており、アプリケーションから様々な操作を行うことができます。
+p MisskeyはWeb APIを公開しており、様々な操作をプログラム上から行うことができます。
+p APIを自分のアカウントから利用する場合と、アプリケーションから利用する場合で利用手順が異なりますので、それぞれのケースについて説明します。
 
 section
 	h2 自分の所有するアカウントからAPIにアクセスする場合
-	p 「設定」で、APIにアクセスするのに必要なAPIキーを取得してください。
+	p 「設定 > API」で、APIにアクセスするのに必要なAPIキーを取得してください。
 	p APIにアクセスする際には、リクエストにAPIキーを「i」というパラメータ名で含めます。
 	p APIの詳しい使用法は「Misskey APIの利用」セクションをご覧ください。
 
 section
 	h2 アプリケーションからAPIにアクセスする場合
 	p
-		| あなたのWebサービスやアプリケーションなどからMisskey APIを利用したい場合、
-		| ユーザーにアカウントへのアクセスを許可してもらい、ユーザーのアクセストークンを取得する必要があります。
-	p アクセストークンを取得するまでの流れを説明します。
+		| 直接ユーザーのAPIキーをアプリケーションが扱うのは危険なので、
+		| アプリケーションからAPIを利用する際には、アプリケーションとアプリケーションを利用するユーザーが結び付けられた専用のトークン(アクセストークン)をMisskeyに発行してもらい、
+		| そのトークンをリクエストのパラメータに含める必要があります。
+		| (アクセストークンは、ユーザーが自分のアカウントにあなたのアプリケーションがアクセスすることを許可した場合のみ発行されます)
+
+	p それでは、アクセストークンを取得するまでの流れを説明します。
 
 	section
 		h3 1.アプリケーションを登録する
 		p まず、あなたのWebサービスやアプリケーションをMisskeyに登録します。
-		p デベロッパーセンターから登録を行ってください。
-		p 登録が済むとアプリケーションのシークレットキーが入手できます。
+		p
+			a(href=common.config.dev_url, target="_blank") デベロッパーセンター
+			| にアクセスし、「アプリ > アプリ作成」に進みます。
+			| フォームに必要事項を記入し、アプリを作成してください。フォームの記入欄の説明は以下の通りです:
+
+		table
+			thead
+				tr
+					th 名前
+					th 説明
+			tbody
+				tr
+					td アプリケーション名
+					td あなたのアプリケーションやWebサービスの名称。
+				tr
+					td アプリの概要
+					td あなたのアプリケーションやWebサービスの簡単な説明や紹介。
+				tr
+					td コールバックURL
+					td あなたのアプリケーションがWebサービスである場合、ユーザーが後述する認証フォームで認証を終えた際にリダイレクトするURLを設定できます。
+				tr
+					td 権限
+					td あなたのアプリケーションやWebサービスが要求する権限。ここで要求した機能だけがAPIからアクセスできます。
+
+		p
+			| 登録が済むとアプリケーションのシークレットキーが入手できます。このシークレットキーは後で使用します。
+			| アプリに成りすまされる可能性があるため、極力このシークレットキーは公開しないようにしてください。
 
 	section
 		h3 2.ユーザーに認証させる
@@ -64,3 +93,7 @@ section
 	h2 Misskey APIの利用
 	p APIはすべてリクエストのパラメータ・レスポンスともにJSON形式です。また、すべてのエンドポイントはPOSTメソッドのみ受け付けます。
 	p APIリファレンスもご確認ください。
+	
+	section
+		h3 レートリミット
+		p Misskey APIにはレートリミットがあり、短時間のうちに多数のリクエストを送信すると、一定時間APIを利用することができなくなることがあります。

From 5c36423841975b173dba874bde0b6d3b9633e2a3 Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Sun, 17 Dec 2017 13:04:19 +0900
Subject: [PATCH 101/186] Update api.ja.pug

---
 src/web/docs/api.ja.pug | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/web/docs/api.ja.pug b/src/web/docs/api.ja.pug
index dbe17fe66..5514a4097 100644
--- a/src/web/docs/api.ja.pug
+++ b/src/web/docs/api.ja.pug
@@ -1,7 +1,7 @@
 h1 Misskey API
 
 p MisskeyはWeb APIを公開しており、様々な操作をプログラム上から行うことができます。
-p APIを自分のアカウントから利用する場合と、アプリケーションから利用する場合で利用手順が異なりますので、それぞれのケースについて説明します。
+p APIを自分のアカウントから利用する場合(自分のアカウントのみ操作したい場合)と、アプリケーションから利用する場合(不特定のアカウントを操作したい場合)とで利用手順が異なりますので、それぞれのケースについて説明します。
 
 section
 	h2 自分の所有するアカウントからAPIにアクセスする場合
@@ -85,7 +85,7 @@ section
 					td セッションのトークン
 		p 上手くいけば、認証したユーザーのアクセストークンがレスポンスとして取得できます。おめでとうございます!
 
-	p アクセストークンが取得できたら、「ユーザーのアクセストークン+アプリのシークレットキーをsha256したもの」を「i」というパラメータでリクエストに含めるだけで、APIにアクセスできます。
+	p アクセストークンが取得できたら、「ユーザーのアクセストークン+アプリのシークレットキーをsha256したもの」を「i」というパラメータでリクエストに含めると、APIにアクセスすることができます。
 
 	p APIの詳しい使用法は「Misskey APIの利用」セクションをご覧ください。
 

From d4fb399c95c65e4a6805e02074b8e5cc754a3822 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 17 Dec 2017 14:35:30 +0900
Subject: [PATCH 102/186] =?UTF-8?q?=E3=81=AA=E3=82=93=E3=81=8B=E3=82=82?=
 =?UTF-8?q?=E3=81=86=E3=82=81=E3=81=A3=E3=81=A1=E3=82=83=E5=A4=89=E3=81=88?=
 =?UTF-8?q?=E3=81=9F?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 gulpfile.ts                         | 12 +-----
 src/common/build/fa.ts              | 57 +++++++++++++++++++++++++++++
 src/common/build/i18n.ts            | 50 +++++++++++++++++++++++++
 src/web/app/desktop/ui.styl         |  2 +-
 src/web/const.styl                  |  4 ++
 src/web/docs/api.ja.pug             | 10 ++---
 src/web/docs/api/endpoints/view.pug |  4 +-
 src/web/docs/api/entities/view.pug  |  2 +-
 src/web/docs/api/gulpfile.ts        | 11 +++++-
 src/web/docs/api/mixins.pug         | 12 +++---
 src/web/docs/gulpfile.ts            |  8 ++--
 src/web/docs/layout.pug             |  7 +++-
 src/web/docs/style.styl             |  1 +
 src/web/docs/ui.styl                | 19 ++++++++++
 src/web/docs/vars.ts                |  7 ++--
 src/web/style.styl                  |  5 +--
 webpack/module/index.ts             |  4 +-
 webpack/module/rules/fa.ts          | 47 +-----------------------
 webpack/module/rules/i18n.ts        | 33 ++---------------
 webpack/module/rules/index.ts       |  4 +-
 webpack/webpack.config.ts           |  4 +-
 21 files changed, 185 insertions(+), 118 deletions(-)
 create mode 100644 src/common/build/fa.ts
 create mode 100644 src/common/build/i18n.ts
 create mode 100644 src/web/const.styl
 create mode 100644 src/web/docs/ui.styl

diff --git a/gulpfile.ts b/gulpfile.ts
index 3b7a12640..21870473e 100644
--- a/gulpfile.ts
+++ b/gulpfile.ts
@@ -20,16 +20,8 @@ import * as mocha from 'gulp-mocha';
 import * as replace from 'gulp-replace';
 import * as htmlmin from 'gulp-htmlmin';
 const uglifyes = require('uglify-es');
-import * as fontawesome from '@fortawesome/fontawesome';
-import * as regular from '@fortawesome/fontawesome-free-regular';
-import * as solid from '@fortawesome/fontawesome-free-solid';
-import * as brands from '@fortawesome/fontawesome-free-brands';
-
-// Add icons
-fontawesome.library.add(regular);
-fontawesome.library.add(solid);
-fontawesome.library.add(brands);
 
+import { fa } from './src/common/build/fa';
 import version from './src/version';
 import config from './src/conf';
 
@@ -179,7 +171,7 @@ gulp.task('build:client:pug', [
 			.pipe(pug({
 				locals: {
 					themeColor: constants.themeColor,
-					facss: fontawesome.dom.css(),
+					facss: fa.dom.css(),
 					//hljscss: fs.readFileSync('./node_modules/highlight.js/styles/default.css', 'utf8')
 					hljscss: fs.readFileSync('./src/web/assets/code-highlight.css', 'utf8')
 				}
diff --git a/src/common/build/fa.ts b/src/common/build/fa.ts
new file mode 100644
index 000000000..0c21be950
--- /dev/null
+++ b/src/common/build/fa.ts
@@ -0,0 +1,57 @@
+/**
+ * Replace fontawesome symbols
+ */
+
+import * as fontawesome from '@fortawesome/fontawesome';
+import * as regular from '@fortawesome/fontawesome-free-regular';
+import * as solid from '@fortawesome/fontawesome-free-solid';
+import * as brands from '@fortawesome/fontawesome-free-brands';
+
+// Add icons
+fontawesome.library.add(regular);
+fontawesome.library.add(solid);
+fontawesome.library.add(brands);
+
+export const pattern = /%fa:(.+?)%/g;
+
+export const replacement = (_, key) => {
+	const args = key.split(' ');
+	let prefix = 'fas';
+	const classes = [];
+	let transform = '';
+	let name;
+
+	args.forEach(arg => {
+		if (arg == 'R' || arg == 'S' || arg == 'B') {
+			prefix =
+				arg == 'R' ? 'far' :
+				arg == 'S' ? 'fas' :
+				arg == 'B' ? 'fab' :
+				'';
+		} else if (arg[0] == '.') {
+			classes.push('fa-' + arg.substr(1));
+		} else if (arg[0] == '-') {
+			transform = arg.substr(1).split('|').join(' ');
+		} else {
+			name = arg;
+		}
+	});
+
+	const icon = fontawesome.icon({ prefix, iconName: name }, {
+		classes: classes
+	});
+
+	if (icon) {
+		icon.transform = fontawesome.parse.transform(transform);
+		return `<i data-fa class="${name}">${icon.html[0]}</i>`;
+	} else {
+		console.warn(`'${name}' not found in fa`);
+		return '';
+	}
+};
+
+export default (src: string) => {
+	return src.replace(pattern, replacement);
+};
+
+export const fa = fontawesome;
diff --git a/src/common/build/i18n.ts b/src/common/build/i18n.ts
new file mode 100644
index 000000000..1ae22147c
--- /dev/null
+++ b/src/common/build/i18n.ts
@@ -0,0 +1,50 @@
+/**
+ * Replace i18n texts
+ */
+
+import locale from '../../../locales';
+
+export default class Replacer {
+	private lang: string;
+
+	public pattern = /"%i18n:(.+?)%"|'%i18n:(.+?)%'|%i18n:(.+?)%/g;
+
+	constructor(lang: string) {
+		this.lang = lang;
+
+		this.get = this.get.bind(this);
+		this.replacement = this.replacement.bind(this);
+	}
+
+	private get(key: string) {
+		let text = locale[this.lang];
+
+		// Check the key existance
+		const error = key.split('.').some(k => {
+			if (text.hasOwnProperty(k)) {
+				text = text[k];
+				return false;
+			} else {
+				return true;
+			}
+		});
+
+		if (error) {
+			console.warn(`key '${key}' not found in '${this.lang}'`);
+			return key; // Fallback
+		} else {
+			return text;
+		}
+	}
+
+	public replacement(match, a, b, c) {
+		const key = a || b || c;
+		if (match[0] == '"') {
+			return '"' + this.get(key).replace(/"/g, '\\"') + '"';
+		} else if (match[0] == "'") {
+			return '\'' + this.get(key).replace(/'/g, '\\\'') + '\'';
+		} else {
+			return this.get(key);
+		}
+	}
+}
diff --git a/src/web/app/desktop/ui.styl b/src/web/app/desktop/ui.styl
index cb98bf06a..058271876 100644
--- a/src/web/app/desktop/ui.styl
+++ b/src/web/app/desktop/ui.styl
@@ -1,4 +1,4 @@
-@import "../app"
+@import "../../const"
 
 button
 	font-family sans-serif
diff --git a/src/web/const.styl b/src/web/const.styl
new file mode 100644
index 000000000..b6560701d
--- /dev/null
+++ b/src/web/const.styl
@@ -0,0 +1,4 @@
+json('../const.json')
+
+$theme-color = themeColor
+$theme-color-foreground = themeColorForeground
diff --git a/src/web/docs/api.ja.pug b/src/web/docs/api.ja.pug
index 5514a4097..2584b0858 100644
--- a/src/web/docs/api.ja.pug
+++ b/src/web/docs/api.ja.pug
@@ -7,6 +7,7 @@ section
 	h2 自分の所有するアカウントからAPIにアクセスする場合
 	p 「設定 > API」で、APIにアクセスするのに必要なAPIキーを取得してください。
 	p APIにアクセスする際には、リクエストにAPIキーを「i」というパラメータ名で含めます。
+	div.ui.info.warn: p %fa:exclamation-triangle%アカウントを不正利用される可能性があるため、このトークンは第三者に教えないでください(アプリなどにも入力しないでください)。
 	p APIの詳しい使用法は「Misskey APIの利用」セクションをご覧ください。
 
 section
@@ -15,7 +16,7 @@ section
 		| 直接ユーザーのAPIキーをアプリケーションが扱うのは危険なので、
 		| アプリケーションからAPIを利用する際には、アプリケーションとアプリケーションを利用するユーザーが結び付けられた専用のトークン(アクセストークン)をMisskeyに発行してもらい、
 		| そのトークンをリクエストのパラメータに含める必要があります。
-		| (アクセストークンは、ユーザーが自分のアカウントにあなたのアプリケーションがアクセスすることを許可した場合のみ発行されます)
+	div.ui.info: p %fa:info-circle%アクセストークンは、ユーザーが自分のアカウントにあなたのアプリケーションがアクセスすることを許可した場合のみ発行されます
 
 	p それでは、アクセストークンを取得するまでの流れを説明します。
 
@@ -46,9 +47,8 @@ section
 					td 権限
 					td あなたのアプリケーションやWebサービスが要求する権限。ここで要求した機能だけがAPIからアクセスできます。
 
-		p
-			| 登録が済むとアプリケーションのシークレットキーが入手できます。このシークレットキーは後で使用します。
-			| アプリに成りすまされる可能性があるため、極力このシークレットキーは公開しないようにしてください。
+		p 登録が済むとアプリケーションのシークレットキーが入手できます。このシークレットキーは後で使用します。
+		div.ui.info.warn: p %fa:exclamation-triangle%アプリに成りすまされる可能性があるため、極力このシークレットキーは公開しないようにしてください。
 
 	section
 		h3 2.ユーザーに認証させる
@@ -93,7 +93,7 @@ section
 	h2 Misskey APIの利用
 	p APIはすべてリクエストのパラメータ・レスポンスともにJSON形式です。また、すべてのエンドポイントはPOSTメソッドのみ受け付けます。
 	p APIリファレンスもご確認ください。
-	
+
 	section
 		h3 レートリミット
 		p Misskey APIにはレートリミットがあり、短時間のうちに多数のリクエストを送信すると、一定時間APIを利用することができなくなることがあります。
diff --git a/src/web/docs/api/endpoints/view.pug b/src/web/docs/api/endpoints/view.pug
index 90084ab27..d271a5517 100644
--- a/src/web/docs/api/endpoints/view.pug
+++ b/src/web/docs/api/endpoints/view.pug
@@ -17,7 +17,7 @@ block main
 	p#desc= desc[lang] || desc['ja']
 
 	section
-		h2= common.i18n[lang]['docs']['api']['endpoints']['params']
+		h2 %i18n:docs.api.endpoints.params%
 		+propTable(params)
 
 		if paramDefs
@@ -28,5 +28,5 @@ block main
 
 	if res
 		section
-			h2= common.i18n[lang]['docs']['api']['endpoints']['res']
+			h2 %i18n:docs.api.endpoints.res%
 			+propTable(res)
diff --git a/src/web/docs/api/entities/view.pug b/src/web/docs/api/entities/view.pug
index 99e786c69..2156463dc 100644
--- a/src/web/docs/api/entities/view.pug
+++ b/src/web/docs/api/entities/view.pug
@@ -10,7 +10,7 @@ block main
 	p#desc= desc[lang] || desc['ja']
 
 	section
-		h2= common.i18n[lang]['docs']['api']['entities']['properties']
+		h2 %i18n:docs.api.entities.properties%
 		+propTable(props)
 
 		if propDefs
diff --git a/src/web/docs/api/gulpfile.ts b/src/web/docs/api/gulpfile.ts
index 2e8409c59..4c30871a0 100644
--- a/src/web/docs/api/gulpfile.ts
+++ b/src/web/docs/api/gulpfile.ts
@@ -10,13 +10,16 @@ import * as pug from 'pug';
 import * as yaml from 'js-yaml';
 import * as mkdirp from 'mkdirp';
 
+import locales from '../../../../locales';
+import I18nReplacer from '../../../common/build/i18n';
+import fa from '../../../common/build/fa';
 import config from './../../../conf';
 
 import generateVars from '../vars';
 
 const commonVars = generateVars();
 
-const langs = Object.keys(commonVars.i18n);
+const langs = Object.keys(locales);
 
 const kebab = string => string.replace(/([a-z])([A-Z])/g, '$1-$2').replace(/\s+/g, '-').toLowerCase();
 
@@ -124,6 +127,9 @@ gulp.task('doc:api:endpoints', () => {
 						console.error(renderErr);
 						return;
 					}
+					const i18n = new I18nReplacer(lang);
+					html = html.replace(i18n.pattern, i18n.replacement);
+					html = fa(html);
 					const htmlPath = `./built/web/docs/${lang}/api/endpoints/${ep.endpoint}.html`;
 					mkdirp(path.dirname(htmlPath), (mkdirErr) => {
 						if (mkdirErr) {
@@ -164,6 +170,9 @@ gulp.task('doc:api:entities', () => {
 						console.error(renderErr);
 						return;
 					}
+					const i18n = new I18nReplacer(lang);
+					html = html.replace(i18n.pattern, i18n.replacement);
+					html = fa(html);
 					const htmlPath = `./built/web/docs/${lang}/api/entities/${kebab(entity.name)}.html`;
 					mkdirp(path.dirname(htmlPath), (mkdirErr) => {
 						if (mkdirErr) {
diff --git a/src/web/docs/api/mixins.pug b/src/web/docs/api/mixins.pug
index b563a121d..686bf6a2b 100644
--- a/src/web/docs/api/mixins.pug
+++ b/src/web/docs/api/mixins.pug
@@ -1,10 +1,10 @@
 mixin propTable(props)
 	table.props
 		thead: tr
-			th= common.i18n[lang]['docs']['api']['props']['name']
-			th= common.i18n[lang]['docs']['api']['props']['type']
-			th= common.i18n[lang]['docs']['api']['props']['optional']
-			th= common.i18n[lang]['docs']['api']['props']['description']
+			th %i18n:docs.api.props.name%
+			th %i18n:docs.api.props.type%
+			th %i18n:docs.api.props.optional%
+			th %i18n:docs.api.props.description%
 		tbody
 			each prop in props
 				tr
@@ -31,7 +31,7 @@ mixin propTable(props)
 							|  (Date)
 					td.optional
 						if prop.optional
-							= common.i18n[lang]['docs']['api']['props']['yes']
+							| %i18n:docs.api.props.yes%
 						else
-							= common.i18n[lang]['docs']['api']['props']['no']
+							| %i18n:docs.api.props.no%
 					td.desc!= prop.desc[lang] || prop.desc['ja']
diff --git a/src/web/docs/gulpfile.ts b/src/web/docs/gulpfile.ts
index 6668abdec..71033e1bc 100644
--- a/src/web/docs/gulpfile.ts
+++ b/src/web/docs/gulpfile.ts
@@ -7,13 +7,12 @@ import * as path from 'path';
 import * as glob from 'glob';
 import * as gulp from 'gulp';
 import * as pug from 'pug';
-//import * as yaml from 'js-yaml';
 import * as mkdirp from 'mkdirp';
 import stylus = require('gulp-stylus');
 import cssnano = require('gulp-cssnano');
 
-//import config from './../../conf';
-
+import I18nReplacer from '../../common/build/i18n';
+import fa from '../../common/build/fa';
 import generateVars from './vars';
 
 require('./api/gulpfile.ts');
@@ -53,6 +52,9 @@ gulp.task('doc:docs', () => {
 						console.error(renderErr2);
 						return;
 					}
+					const i18n = new I18nReplacer(lang);
+					html = html.replace(i18n.pattern, i18n.replacement);
+					html = fa(html);
 					const htmlPath = `./built/web/docs/${lang}/${name}.html`;
 					mkdirp(path.dirname(htmlPath), (mkdirErr) => {
 						if (mkdirErr) {
diff --git a/src/web/docs/layout.pug b/src/web/docs/layout.pug
index ee8018ec6..9dfd0ab7a 100644
--- a/src/web/docs/layout.pug
+++ b/src/web/docs/layout.pug
@@ -9,6 +9,9 @@ html(lang= lang)
 		link(rel="stylesheet" href="/assets/style.css")
 		block meta
 
+		//- FontAwesome style
+		style #{common.facss}
+
 	body
 		nav
 			ul
@@ -33,6 +36,6 @@ html(lang= lang)
 
 			footer
 				p
-					= common.i18n[lang]['docs']['edit-this-page-on-github']
-					a(href=src target="_blank")= common.i18n[lang]['docs']['edit-this-page-on-github-link']
+					| %i18n:docs.edit-this-page-on-github%
+					a(href=src target="_blank") %i18n:docs.edit-this-page-on-github-link%
 				small= common.copyright
diff --git a/src/web/docs/style.styl b/src/web/docs/style.styl
index 32a2264f1..414be5c53 100644
--- a/src/web/docs/style.styl
+++ b/src/web/docs/style.styl
@@ -1,4 +1,5 @@
 @import "../style"
+@import "./ui"
 
 body
 	margin 0
diff --git a/src/web/docs/ui.styl b/src/web/docs/ui.styl
new file mode 100644
index 000000000..8d5515712
--- /dev/null
+++ b/src/web/docs/ui.styl
@@ -0,0 +1,19 @@
+.ui.info
+	display block
+	margin 1em 0
+	padding 0 1em
+	font-size 90%
+	color rgba(#000, 0.87)
+	background #f8f8f9
+	border-radius 4px
+	overflow hidden
+
+	> p
+		opacity 0.8
+
+		> [data-fa]:first-child
+			margin-right 0.25em
+
+	&.warn
+		color #573a08
+		background #FFFAF3
diff --git a/src/web/docs/vars.ts b/src/web/docs/vars.ts
index da590d7bd..65b224fbf 100644
--- a/src/web/docs/vars.ts
+++ b/src/web/docs/vars.ts
@@ -1,7 +1,8 @@
 import * as fs from 'fs';
 import * as glob from 'glob';
 import * as yaml from 'js-yaml';
-import langs from '../../../locales';
+
+import { fa } from '../../common/build/fa';
 import config from '../../conf';
 const constants = require('../../const.json');
 
@@ -37,9 +38,9 @@ export default function(): { [key: string]: any } {
 
 	vars['config'] = config;
 
-	vars['i18n'] = langs;
-
 	vars['copyright'] = constants.copyright;
 
+	vars['facss'] = fa.dom.css();
+
 	return vars;
 }
diff --git a/src/web/style.styl b/src/web/style.styl
index 573df10d7..c25fc8fb5 100644
--- a/src/web/style.styl
+++ b/src/web/style.styl
@@ -1,9 +1,6 @@
-json('../const.json')
-
 @charset 'utf-8'
 
-$theme-color = themeColor
-$theme-color-foreground = themeColorForeground
+@import "./const"
 
 /*
 	::selection
diff --git a/webpack/module/index.ts b/webpack/module/index.ts
index 15f36557c..088aca723 100644
--- a/webpack/module/index.ts
+++ b/webpack/module/index.ts
@@ -1,5 +1,5 @@
 import rules from './rules';
 
-export default (lang, locale) => ({
-	rules: rules(lang, locale)
+export default lang => ({
+	rules: rules(lang)
 });
diff --git a/webpack/module/rules/fa.ts b/webpack/module/rules/fa.ts
index 47c72a28a..891b78ece 100644
--- a/webpack/module/rules/fa.ts
+++ b/webpack/module/rules/fa.ts
@@ -3,16 +3,7 @@
  */
 
 const StringReplacePlugin = require('string-replace-webpack-plugin');
-
-import * as fontawesome from '@fortawesome/fontawesome';
-import * as regular from '@fortawesome/fontawesome-free-regular';
-import * as solid from '@fortawesome/fontawesome-free-solid';
-import * as brands from '@fortawesome/fontawesome-free-brands';
-
-// Add icons
-fontawesome.library.add(regular);
-fontawesome.library.add(solid);
-fontawesome.library.add(brands);
+import { pattern, replacement } from '../../../src/common/build/fa';
 
 export default () => ({
 	enforce: 'pre',
@@ -20,41 +11,7 @@ export default () => ({
 	exclude: /node_modules/,
 	loader: StringReplacePlugin.replace({
 		replacements: [{
-			pattern: /%fa:(.+?)%/g, replacement: (_, key) => {
-				const args = key.split(' ');
-				let prefix = 'fas';
-				const classes = [];
-				let transform = '';
-				let name;
-
-				args.forEach(arg => {
-					if (arg == 'R' || arg == 'S' || arg == 'B') {
-						prefix =
-							arg == 'R' ? 'far' :
-							arg == 'S' ? 'fas' :
-							arg == 'B' ? 'fab' :
-							'';
-					} else if (arg[0] == '.') {
-						classes.push('fa-' + arg.substr(1));
-					} else if (arg[0] == '-') {
-						transform = arg.substr(1).split('|').join(' ');
-					} else {
-						name = arg;
-					}
-				});
-
-				const icon = fontawesome.icon({ prefix, iconName: name }, {
-					classes: classes
-				});
-
-				if (icon) {
-					icon.transform = fontawesome.parse.transform(transform);
-					return `<i data-fa class="${name}">${icon.html[0]}</i>`;
-				} else {
-					console.warn(`'${name}' not found in fa`);
-					return '';
-				}
-			}
+			pattern, replacement
 		}]
 	})
 });
diff --git a/webpack/module/rules/i18n.ts b/webpack/module/rules/i18n.ts
index aa4e58448..7261548be 100644
--- a/webpack/module/rules/i18n.ts
+++ b/webpack/module/rules/i18n.ts
@@ -3,28 +3,10 @@
  */
 
 const StringReplacePlugin = require('string-replace-webpack-plugin');
+import Replacer from '../../../src/common/build/i18n';
 
-export default (lang, locale) => {
-	function get(key: string) {
-		let text = locale;
-
-		// Check the key existance
-		const error = key.split('.').some(k => {
-			if (text.hasOwnProperty(k)) {
-				text = text[k];
-				return false;
-			} else {
-				return true;
-			}
-		});
-
-		if (error) {
-			console.warn(`key '${key}' not found in '${lang}'`);
-			return key; // Fallback
-		} else {
-			return text;
-		}
-	}
+export default lang => {
+	const replacer = new Replacer(lang);
 
 	return {
 		enforce: 'pre',
@@ -32,14 +14,7 @@ export default (lang, locale) => {
 		exclude: /node_modules/,
 		loader: StringReplacePlugin.replace({
 			replacements: [{
-				pattern: /"%i18n:(.+?)%"/g, replacement: (_, key) =>
-					'"' + get(key).replace(/"/g, '\\"') + '"'
-			}, {
-				pattern: /'%i18n:(.+?)%'/g, replacement: (_, key) =>
-					'\'' + get(key).replace(/'/g, '\\\'') + '\''
-			}, {
-				pattern: /%i18n:(.+?)%/g, replacement: (_, key) =>
-					get(key)
+				pattern: replacer.pattern, replacement: replacer.replacement
 			}]
 		})
 	};
diff --git a/webpack/module/rules/index.ts b/webpack/module/rules/index.ts
index b6a0a5e2e..b02bdef72 100644
--- a/webpack/module/rules/index.ts
+++ b/webpack/module/rules/index.ts
@@ -7,8 +7,8 @@ import tag from './tag';
 import stylus from './stylus';
 import typescript from './typescript';
 
-export default (lang, locale) => [
-	i18n(lang, locale),
+export default lang => [
+	i18n(lang),
 	license(),
 	fa(),
 	base64(),
diff --git a/webpack/webpack.config.ts b/webpack/webpack.config.ts
index 124bd975b..d67b8ef77 100644
--- a/webpack/webpack.config.ts
+++ b/webpack/webpack.config.ts
@@ -8,7 +8,7 @@ import plugins from './plugins';
 import langs from '../locales';
 import version from '../src/version';
 
-module.exports = Object.entries(langs).map(([lang, locale]) => {
+module.exports = Object.keys(langs).map(lang => {
 	// Chunk name
 	const name = lang;
 
@@ -32,7 +32,7 @@ module.exports = Object.entries(langs).map(([lang, locale]) => {
 	return {
 		name,
 		entry,
-		module: module_(lang, locale),
+		module: module_(lang),
 		plugins: plugins(version, lang),
 		output,
 		resolve: {

From 3ab4f6b795a2ab55359baf1af151263d3f62f9ab Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 17 Dec 2017 14:35:54 +0900
Subject: [PATCH 103/186] v3404

---
 CHANGELOG.md | 4 ++++
 package.json | 2 +-
 2 files changed, 5 insertions(+), 1 deletion(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index d3be42879..e9ae2cb17 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,10 @@ ChangeLog (Release Notes)
 =========================
 主に notable な changes を書いていきます
 
+3404 (2017/12/17)
+-----------------
+* なんか
+
 3400 (2017/12/17)
 -----------------
 * なんか
diff --git a/package.json b/package.json
index 181c20a03..ef5a76059 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
 	"name": "misskey",
 	"author": "syuilo <i@syuilo.com>",
-	"version": "0.0.3400",
+	"version": "0.0.3404",
 	"license": "MIT",
 	"description": "A miniblog-based SNS",
 	"bugs": "https://github.com/syuilo/misskey/issues",

From 49fff2137a532f889016556e4210d36b2110e9a5 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 17 Dec 2017 15:15:54 +0900
Subject: [PATCH 104/186] =?UTF-8?q?=E3=83=AC=E3=82=B9=E3=83=9D=E3=83=B3?=
 =?UTF-8?q?=E3=82=B7=E3=83=96=E3=81=AB?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 src/web/docs/style.styl | 28 +++++++++++++++++++++++-----
 1 file changed, 23 insertions(+), 5 deletions(-)

diff --git a/src/web/docs/style.styl b/src/web/docs/style.styl
index 414be5c53..d2f91fb7c 100644
--- a/src/web/docs/style.styl
+++ b/src/web/docs/style.styl
@@ -4,6 +4,7 @@
 body
 	margin 0
 	color #34495e
+	word-break break-word
 
 main
 	margin 0 0 0 256px
@@ -49,16 +50,37 @@ main
 nav
 	display block
 	position fixed
+	z-index 10000
 	top 0
 	left 0
 	width 256px
 	height 100%
 	overflow auto
 	padding 32px
+	background #fff
 	border-right solid 2px #eee
 
+@media (max-width 1025px)
+	main
+		margin 0
+		max-width 100%
+
+	nav
+		position relative
+		width 100%
+		max-height 128px
+		background #f9f9f9
+		border-right none
+
+@media (max-width 512px)
+	main
+		padding 16px
+
 table
+	display block
 	width 100%
+	max-width 100%
+	overflow auto
 	border-spacing 0
 	border-collapse collapse
 
@@ -68,12 +90,7 @@ table
 
 		tr
 			th
-				position sticky
-				top 0
-				z-index 1
 				text-align left
-				box-shadow 0 1px 0 0 #eee
-				background #fff
 
 	tbody
 		tr
@@ -82,3 +99,4 @@ table
 
 	th, td
 		padding 8px 16px
+		min-width 128px

From 96482d5617b24749b61c20a8bbbba77d0267f65f Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Sun, 17 Dec 2017 10:41:49 +0000
Subject: [PATCH 105/186] fix(package): update cropperjs to version 1.2.1

Closes #1006
---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index ef5a76059..9f3e1eb38 100644
--- a/package.json
+++ b/package.json
@@ -90,7 +90,7 @@
 		"compression": "1.7.1",
 		"cookie": "0.3.1",
 		"cors": "2.8.4",
-		"cropperjs": "1.1.3",
+		"cropperjs": "1.2.1",
 		"css-loader": "0.28.7",
 		"debug": "3.1.0",
 		"deep-equal": "1.0.1",

From 71d359bc46cb806a3bd53f65ecc1e1253839225a Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Mon, 18 Dec 2017 00:37:19 +0900
Subject: [PATCH 106/186] Update api.ja.pug

---
 src/web/docs/api.ja.pug | 22 +++++++++++-----------
 1 file changed, 11 insertions(+), 11 deletions(-)

diff --git a/src/web/docs/api.ja.pug b/src/web/docs/api.ja.pug
index 2584b0858..78749e83e 100644
--- a/src/web/docs/api.ja.pug
+++ b/src/web/docs/api.ja.pug
@@ -22,7 +22,7 @@ section
 
 	section
 		h3 1.アプリケーションを登録する
-		p まず、あなたのWebサービスやアプリケーションをMisskeyに登録します。
+		p まず、あなたのアプリケーションやWebサービス(以後、あなたのアプリと呼びます)をMisskeyに登録します。
 		p
 			a(href=common.config.dev_url, target="_blank") デベロッパーセンター
 			| にアクセスし、「アプリ > アプリ作成」に進みます。
@@ -36,23 +36,23 @@ section
 			tbody
 				tr
 					td アプリケーション名
-					td あなたのアプリケーションやWebサービスの名称。
+					td あなたのアプリの名称。
 				tr
 					td アプリの概要
-					td あなたのアプリケーションやWebサービスの簡単な説明や紹介。
+					td あなたのアプリの簡単な説明や紹介。
 				tr
 					td コールバックURL
-					td あなたのアプリケーションがWebサービスである場合、ユーザーが後述する認証フォームで認証を終えた際にリダイレクトするURLを設定できます。
+					td あなたのアプリがWebサービスである場合、ユーザーが後述する認証フォームで認証を終えた際にリダイレクトするURLを設定できます。
 				tr
 					td 権限
-					td あなたのアプリケーションやWebサービスが要求する権限。ここで要求した機能だけがAPIからアクセスできます。
+					td あなたのアプリが要求する権限。ここで要求した機能だけがAPIからアクセスできます。
 
-		p 登録が済むとアプリケーションのシークレットキーが入手できます。このシークレットキーは後で使用します。
+		p 登録が済むとあなたのアプリのシークレットキーが入手できます。このシークレットキーは後で使用します。
 		div.ui.info.warn: p %fa:exclamation-triangle%アプリに成りすまされる可能性があるため、極力このシークレットキーは公開しないようにしてください。
 
 	section
 		h3 2.ユーザーに認証させる
-		p あなたのアプリケーションを使ってもらうには、ユーザーにアカウントへのアクセスの許可をもらう必要があります。
+		p あなたのアプリを使ってもらうには、ユーザーにアカウントへのアクセスの許可をもらう必要があります。
 		p
 			| 認証セッションを開始するには、#{common.config.api_url}/auth/session/generate へパラメータに app_secret としてシークレットキーを含めたリクエストを送信します。
 			| リクエスト形式はJSONで、メソッドはPOSTです。
@@ -60,10 +60,10 @@ section
 
 		p
 			| あなたのアプリがコールバックURLを設定している場合、
-			| ユーザーがアプリの連携を許可すると設定しているコールバックURLに token という名前でセッションのトークンが含まれたクエリを付けてリダイレクトします。
+			| ユーザーがあなたのアプリの連携を許可すると設定しているコールバックURLに token という名前でセッションのトークンが含まれたクエリを付けてリダイレクトします。
 
 		p
-			| あなたのアプリがコールバックURLを設定していない場合、ユーザーがアプリの連携を許可したことを(何らかの方法で(たとえばボタンを押させるなど))確認出来るようにしてください。
+			| あなたのアプリがコールバックURLを設定していない場合、ユーザーがあなたのアプリの連携を許可したことを(何らかの方法で(たとえばボタンを押させるなど))確認出来るようにしてください。
 
 	section
 		h3 3.ユーザーのアクセストークンを取得する
@@ -78,14 +78,14 @@ section
 				tr
 					td app_secret
 					td string
-					td アプリのシークレットキー
+					td あなたのアプリのシークレットキー
 				tr
 					td token
 					td string
 					td セッションのトークン
 		p 上手くいけば、認証したユーザーのアクセストークンがレスポンスとして取得できます。おめでとうございます!
 
-	p アクセストークンが取得できたら、「ユーザーのアクセストークン+アプリのシークレットキーをsha256したもの」を「i」というパラメータでリクエストに含めると、APIにアクセスすることができます。
+	p アクセストークンが取得できたら、「ユーザーのアクセストークン+あなたのアプリのシークレットキーをsha256したもの」を「i」というパラメータでリクエストに含めると、APIにアクセスすることができます。
 
 	p APIの詳しい使用法は「Misskey APIの利用」セクションをご覧ください。
 

From 7a17723a0c89fd64d3180748953506967102c1c9 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 18 Dec 2017 04:17:18 +0900
Subject: [PATCH 107/186] :v:

---
 src/web/docs/api.ja.pug |  4 ++++
 src/web/docs/style.styl | 11 +++++++++++
 2 files changed, 15 insertions(+)

diff --git a/src/web/docs/api.ja.pug b/src/web/docs/api.ja.pug
index 78749e83e..167098111 100644
--- a/src/web/docs/api.ja.pug
+++ b/src/web/docs/api.ja.pug
@@ -87,6 +87,10 @@ section
 
 	p アクセストークンが取得できたら、「ユーザーのアクセストークン+あなたのアプリのシークレットキーをsha256したもの」を「i」というパラメータでリクエストに含めると、APIにアクセスすることができます。
 
+	p 「i」パラメータの生成方法を擬似コードで表すと次のようになります:
+	pre: code
+		| const i = sha256(accessToken + secretKey);
+
 	p APIの詳しい使用法は「Misskey APIの利用」セクションをご覧ください。
 
 section
diff --git a/src/web/docs/style.styl b/src/web/docs/style.styl
index d2f91fb7c..27c93a99e 100644
--- a/src/web/docs/style.styl
+++ b/src/web/docs/style.styl
@@ -100,3 +100,14 @@ table
 	th, td
 		padding 8px 16px
 		min-width 128px
+
+code
+	padding 8px 10px
+	font-family Consolas, 'Courier New', Courier, Monaco, monospace
+	color #295c92
+	background #f2f2f2
+	border-radius 4px
+
+pre
+	> code
+		display block

From 5b6b641890ee0c06bc2f997fadb87d76ae471f5a Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Sun, 17 Dec 2017 22:28:15 +0000
Subject: [PATCH 108/186] fix(package): update riot-tag-loader to version 1.1.0

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index 9f3e1eb38..6a1157c92 100644
--- a/package.json
+++ b/package.json
@@ -146,7 +146,7 @@
 		"request": "2.83.0",
 		"rimraf": "2.6.2",
 		"riot": "3.7.4",
-		"riot-tag-loader": "1.0.0",
+		"riot-tag-loader": "1.1.0",
 		"rndstr": "1.0.0",
 		"s-age": "1.1.0",
 		"seedrandom": "2.4.3",

From 332d9a98eb630d0e229922ea571c30153d035f0f Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Tue, 19 Dec 2017 13:18:17 +0900
Subject: [PATCH 109/186] :v:

---
 src/web/app/desktop/tags/drive/browser.tag | 6 ++++--
 1 file changed, 4 insertions(+), 2 deletions(-)

diff --git a/src/web/app/desktop/tags/drive/browser.tag b/src/web/app/desktop/tags/drive/browser.tag
index 901daabfd..a60a46b79 100644
--- a/src/web/app/desktop/tags/drive/browser.tag
+++ b/src/web/app/desktop/tags/drive/browser.tag
@@ -18,14 +18,16 @@
 				<virtual each={ folder in folders }>
 					<mk-drive-browser-folder class="folder" folder={ folder }/>
 				</virtual>
-				<div class="padding" each={ folders }></div>
+				<!-- SEE: https://stackoverflow.com/questions/18744164/flex-box-align-last-row-to-grid -->
+				<div class="padding" each={ Array(10).fill(16) }></div>
 				<button if={ moreFolders }>%i18n:desktop.tags.mk-drive-browser.load-more%</button>
 			</div>
 			<div class="files" ref="filesContainer" if={ files.length > 0 }>
 				<virtual each={ file in files }>
 					<mk-drive-browser-file class="file" file={ file }/>
 				</virtual>
-				<div class="padding" each={ files }></div>
+				<!-- SEE: https://stackoverflow.com/questions/18744164/flex-box-align-last-row-to-grid -->
+				<div class="padding" each={ Array(10).fill(16) }></div>
 				<button if={ moreFiles } onclick={ fetchMoreFiles }>%i18n:desktop.tags.mk-drive-browser.load-more%</button>
 			</div>
 			<div class="empty" if={ files.length == 0 && folders.length == 0 && !fetching }>

From aa9793974173bf030c6b52f0d96770d0cff055fd Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Tue, 19 Dec 2017 13:19:06 +0900
Subject: [PATCH 110/186] :art:

---
 src/web/docs/style.styl | 8 ++++++--
 1 file changed, 6 insertions(+), 2 deletions(-)

diff --git a/src/web/docs/style.styl b/src/web/docs/style.styl
index 27c93a99e..3dcb3e169 100644
--- a/src/web/docs/style.styl
+++ b/src/web/docs/style.styl
@@ -8,9 +8,9 @@ body
 
 main
 	margin 0 0 0 256px
-	padding 32px
+	padding 64px
 	width 100%
-	max-width 700px
+	max-width 768px
 
 	section
 		margin 32px 0
@@ -72,6 +72,10 @@ nav
 		background #f9f9f9
 		border-right none
 
+@media (max-width 768px)
+	main
+		padding 32px
+
 @media (max-width 512px)
 	main
 		padding 16px

From 231e5cc064ee7d5a8c1969ecc8d7e5d2cb9cf9f8 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Tue, 19 Dec 2017 13:21:39 +0900
Subject: [PATCH 111/186] v3415

---
 CHANGELOG.md | 4 ++++
 package.json | 2 +-
 2 files changed, 5 insertions(+), 1 deletion(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index e9ae2cb17..bd4f2c1f1 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,10 @@ ChangeLog (Release Notes)
 =========================
 主に notable な changes を書いていきます
 
+3415 (2017/12/19)
+-----------------
+* デザインの調整
+
 3404 (2017/12/17)
 -----------------
 * なんか
diff --git a/package.json b/package.json
index 6a1157c92..fa8310f87 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
 	"name": "misskey",
 	"author": "syuilo <i@syuilo.com>",
-	"version": "0.0.3404",
+	"version": "0.0.3415",
 	"license": "MIT",
 	"description": "A miniblog-based SNS",
 	"bugs": "https://github.com/syuilo/misskey/issues",

From f3aea1e041791c7c26888fe0a1864748f8b6d355 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Tue, 19 Dec 2017 13:25:07 +0900
Subject: [PATCH 112/186] Fix bug

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index fa8310f87..39922127c 100644
--- a/package.json
+++ b/package.json
@@ -146,7 +146,7 @@
 		"request": "2.83.0",
 		"rimraf": "2.6.2",
 		"riot": "3.7.4",
-		"riot-tag-loader": "1.1.0",
+		"riot-tag-loader": "1.0.0",
 		"rndstr": "1.0.0",
 		"s-age": "1.1.0",
 		"seedrandom": "2.4.3",

From fb09710b621bf7f8eeab46ca278e66951cffae15 Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Tue, 19 Dec 2017 16:10:33 +0900
Subject: [PATCH 113/186] Update api.ja.pug

---
 src/web/docs/api.ja.pug | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/web/docs/api.ja.pug b/src/web/docs/api.ja.pug
index 167098111..2bb08f7f3 100644
--- a/src/web/docs/api.ja.pug
+++ b/src/web/docs/api.ja.pug
@@ -42,7 +42,7 @@ section
 					td あなたのアプリの簡単な説明や紹介。
 				tr
 					td コールバックURL
-					td あなたのアプリがWebサービスである場合、ユーザーが後述する認証フォームで認証を終えた際にリダイレクトするURLを設定できます。
+					td ユーザーが後述する認証フォームで認証を終えた際にリダイレクトするURLを設定できます。あなたのアプリがWebサービスである場合に有用です。
 				tr
 					td 権限
 					td あなたのアプリが要求する権限。ここで要求した機能だけがAPIからアクセスできます。

From c378e5fc946722d6e8a88bcf6a47109c31d70116 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 21 Dec 2017 02:20:02 +0900
Subject: [PATCH 114/186] #1021

---
 src/api/endpoints/channels.ts                 | 16 ++++++------
 src/api/endpoints/channels/posts.ts           | 16 ++++++------
 src/api/endpoints/drive/files.ts              | 16 ++++++------
 src/api/endpoints/drive/folders.ts            | 16 ++++++------
 src/api/endpoints/drive/stream.ts             | 16 ++++++------
 src/api/endpoints/i/notifications.ts          | 16 ++++++------
 src/api/endpoints/i/signin_history.ts         | 16 ++++++------
 src/api/endpoints/messaging/messages.ts       | 16 ++++++------
 src/api/endpoints/posts.ts                    | 16 ++++++------
 src/api/endpoints/posts/mentions.ts           | 16 ++++++------
 src/api/endpoints/posts/reposts.ts            | 16 ++++++------
 src/api/endpoints/posts/timeline.ts           | 26 +++++++++----------
 src/api/endpoints/users.ts                    | 16 ++++++------
 src/api/endpoints/users/posts.ts              | 26 +++++++++----------
 src/web/app/common/tags/messaging/room.tag    |  2 +-
 .../desktop/tags/home-widgets/mentions.tag    |  2 +-
 .../desktop/tags/home-widgets/timeline.tag    |  4 +--
 src/web/app/desktop/tags/notifications.tag    |  2 +-
 src/web/app/desktop/tags/user-timeline.tag    |  4 +--
 src/web/app/mobile/tags/drive.tag             |  2 +-
 src/web/app/mobile/tags/home-timeline.tag     |  2 +-
 src/web/app/mobile/tags/notifications.tag     |  2 +-
 src/web/app/mobile/tags/user-timeline.tag     |  2 +-
 .../docs/api/endpoints/posts/timeline.yaml    |  4 +--
 24 files changed, 135 insertions(+), 135 deletions(-)

diff --git a/src/api/endpoints/channels.ts b/src/api/endpoints/channels.ts
index e10c94389..14817d9bd 100644
--- a/src/api/endpoints/channels.ts
+++ b/src/api/endpoints/channels.ts
@@ -21,13 +21,13 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
 	const [sinceId, sinceIdErr] = $(params.since_id).optional.id().$;
 	if (sinceIdErr) return rej('invalid since_id param');
 
-	// Get 'max_id' parameter
-	const [maxId, maxIdErr] = $(params.max_id).optional.id().$;
-	if (maxIdErr) return rej('invalid max_id param');
+	// Get 'until_id' parameter
+	const [untilId, untilIdErr] = $(params.until_id).optional.id().$;
+	if (untilIdErr) return rej('invalid until_id param');
 
-	// Check if both of since_id and max_id is specified
-	if (sinceId && maxId) {
-		return rej('cannot set since_id and max_id');
+	// Check if both of since_id and until_id is specified
+	if (sinceId && untilId) {
+		return rej('cannot set since_id and until_id');
 	}
 
 	// Construct query
@@ -40,9 +40,9 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
 		query._id = {
 			$gt: sinceId
 		};
-	} else if (maxId) {
+	} else if (untilId) {
 		query._id = {
-			$lt: maxId
+			$lt: untilId
 		};
 	}
 
diff --git a/src/api/endpoints/channels/posts.ts b/src/api/endpoints/channels/posts.ts
index 5c071a124..9c2d607ed 100644
--- a/src/api/endpoints/channels/posts.ts
+++ b/src/api/endpoints/channels/posts.ts
@@ -22,13 +22,13 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 	const [sinceId, sinceIdErr] = $(params.since_id).optional.id().$;
 	if (sinceIdErr) return rej('invalid since_id param');
 
-	// Get 'max_id' parameter
-	const [maxId, maxIdErr] = $(params.max_id).optional.id().$;
-	if (maxIdErr) return rej('invalid max_id param');
+	// Get 'until_id' parameter
+	const [untilId, untilIdErr] = $(params.until_id).optional.id().$;
+	if (untilIdErr) return rej('invalid until_id param');
 
-	// Check if both of since_id and max_id is specified
-	if (sinceId && maxId) {
-		return rej('cannot set since_id and max_id');
+	// Check if both of since_id and until_id is specified
+	if (sinceId && untilId) {
+		return rej('cannot set since_id and until_id');
 	}
 
 	// Get 'channel_id' parameter
@@ -58,9 +58,9 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 		query._id = {
 			$gt: sinceId
 		};
-	} else if (maxId) {
+	} else if (untilId) {
 		query._id = {
-			$lt: maxId
+			$lt: untilId
 		};
 	}
 	//#endregion Construct query
diff --git a/src/api/endpoints/drive/files.ts b/src/api/endpoints/drive/files.ts
index b2e094775..3d5f81339 100644
--- a/src/api/endpoints/drive/files.ts
+++ b/src/api/endpoints/drive/files.ts
@@ -22,13 +22,13 @@ module.exports = async (params, user, app) => {
 	const [sinceId, sinceIdErr] = $(params.since_id).optional.id().$;
 	if (sinceIdErr) throw 'invalid since_id param';
 
-	// Get 'max_id' parameter
-	const [maxId, maxIdErr] = $(params.max_id).optional.id().$;
-	if (maxIdErr) throw 'invalid max_id param';
+	// Get 'until_id' parameter
+	const [untilId, untilIdErr] = $(params.until_id).optional.id().$;
+	if (untilIdErr) throw 'invalid until_id param';
 
-	// Check if both of since_id and max_id is specified
-	if (sinceId && maxId) {
-		throw 'cannot set since_id and max_id';
+	// Check if both of since_id and until_id is specified
+	if (sinceId && untilId) {
+		throw 'cannot set since_id and until_id';
 	}
 
 	// Get 'folder_id' parameter
@@ -52,9 +52,9 @@ module.exports = async (params, user, app) => {
 		query._id = {
 			$gt: sinceId
 		};
-	} else if (maxId) {
+	} else if (untilId) {
 		query._id = {
-			$lt: maxId
+			$lt: untilId
 		};
 	}
 	if (type) {
diff --git a/src/api/endpoints/drive/folders.ts b/src/api/endpoints/drive/folders.ts
index d49ef0af0..7944e2c6a 100644
--- a/src/api/endpoints/drive/folders.ts
+++ b/src/api/endpoints/drive/folders.ts
@@ -22,13 +22,13 @@ module.exports = (params, user, app) => new Promise(async (res, rej) => {
 	const [sinceId, sinceIdErr] = $(params.since_id).optional.id().$;
 	if (sinceIdErr) return rej('invalid since_id param');
 
-	// Get 'max_id' parameter
-	const [maxId, maxIdErr] = $(params.max_id).optional.id().$;
-	if (maxIdErr) return rej('invalid max_id param');
+	// Get 'until_id' parameter
+	const [untilId, untilIdErr] = $(params.until_id).optional.id().$;
+	if (untilIdErr) return rej('invalid until_id param');
 
-	// Check if both of since_id and max_id is specified
-	if (sinceId && maxId) {
-		return rej('cannot set since_id and max_id');
+	// Check if both of since_id and until_id is specified
+	if (sinceId && untilId) {
+		return rej('cannot set since_id and until_id');
 	}
 
 	// Get 'folder_id' parameter
@@ -48,9 +48,9 @@ module.exports = (params, user, app) => new Promise(async (res, rej) => {
 		query._id = {
 			$gt: sinceId
 		};
-	} else if (maxId) {
+	} else if (untilId) {
 		query._id = {
-			$lt: maxId
+			$lt: untilId
 		};
 	}
 
diff --git a/src/api/endpoints/drive/stream.ts b/src/api/endpoints/drive/stream.ts
index 7ee255e5d..5b0eb0a0d 100644
--- a/src/api/endpoints/drive/stream.ts
+++ b/src/api/endpoints/drive/stream.ts
@@ -21,13 +21,13 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 	const [sinceId, sinceIdErr] = $(params.since_id).optional.id().$;
 	if (sinceIdErr) return rej('invalid since_id param');
 
-	// Get 'max_id' parameter
-	const [maxId, maxIdErr] = $(params.max_id).optional.id().$;
-	if (maxIdErr) return rej('invalid max_id param');
+	// Get 'until_id' parameter
+	const [untilId, untilIdErr] = $(params.until_id).optional.id().$;
+	if (untilIdErr) return rej('invalid until_id param');
 
-	// Check if both of since_id and max_id is specified
-	if (sinceId && maxId) {
-		return rej('cannot set since_id and max_id');
+	// Check if both of since_id and until_id is specified
+	if (sinceId && untilId) {
+		return rej('cannot set since_id and until_id');
 	}
 
 	// Get 'type' parameter
@@ -46,9 +46,9 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 		query._id = {
 			$gt: sinceId
 		};
-	} else if (maxId) {
+	} else if (untilId) {
 		query._id = {
-			$lt: maxId
+			$lt: untilId
 		};
 	}
 	if (type) {
diff --git a/src/api/endpoints/i/notifications.ts b/src/api/endpoints/i/notifications.ts
index 607e0768a..48254e5e6 100644
--- a/src/api/endpoints/i/notifications.ts
+++ b/src/api/endpoints/i/notifications.ts
@@ -36,13 +36,13 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 	const [sinceId, sinceIdErr] = $(params.since_id).optional.id().$;
 	if (sinceIdErr) return rej('invalid since_id param');
 
-	// Get 'max_id' parameter
-	const [maxId, maxIdErr] = $(params.max_id).optional.id().$;
-	if (maxIdErr) return rej('invalid max_id param');
+	// Get 'until_id' parameter
+	const [untilId, untilIdErr] = $(params.until_id).optional.id().$;
+	if (untilIdErr) return rej('invalid until_id param');
 
-	// Check if both of since_id and max_id is specified
-	if (sinceId && maxId) {
-		return rej('cannot set since_id and max_id');
+	// Check if both of since_id and until_id is specified
+	if (sinceId && untilId) {
+		return rej('cannot set since_id and until_id');
 	}
 
 	const query = {
@@ -73,9 +73,9 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 		query._id = {
 			$gt: sinceId
 		};
-	} else if (maxId) {
+	} else if (untilId) {
 		query._id = {
-			$lt: maxId
+			$lt: untilId
 		};
 	}
 
diff --git a/src/api/endpoints/i/signin_history.ts b/src/api/endpoints/i/signin_history.ts
index 1a6e50c7c..e38bfa4d9 100644
--- a/src/api/endpoints/i/signin_history.ts
+++ b/src/api/endpoints/i/signin_history.ts
@@ -21,13 +21,13 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 	const [sinceId, sinceIdErr] = $(params.since_id).optional.id().$;
 	if (sinceIdErr) return rej('invalid since_id param');
 
-	// Get 'max_id' parameter
-	const [maxId, maxIdErr] = $(params.max_id).optional.id().$;
-	if (maxIdErr) return rej('invalid max_id param');
+	// Get 'until_id' parameter
+	const [untilId, untilIdErr] = $(params.until_id).optional.id().$;
+	if (untilIdErr) return rej('invalid until_id param');
 
-	// Check if both of since_id and max_id is specified
-	if (sinceId && maxId) {
-		return rej('cannot set since_id and max_id');
+	// Check if both of since_id and until_id is specified
+	if (sinceId && untilId) {
+		return rej('cannot set since_id and until_id');
 	}
 
 	const query = {
@@ -43,9 +43,9 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 		query._id = {
 			$gt: sinceId
 		};
-	} else if (maxId) {
+	} else if (untilId) {
 		query._id = {
-			$lt: maxId
+			$lt: untilId
 		};
 	}
 
diff --git a/src/api/endpoints/messaging/messages.ts b/src/api/endpoints/messaging/messages.ts
index 7b270924e..3d3c6950a 100644
--- a/src/api/endpoints/messaging/messages.ts
+++ b/src/api/endpoints/messaging/messages.ts
@@ -44,13 +44,13 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 	const [sinceId, sinceIdErr] = $(params.since_id).optional.id().$;
 	if (sinceIdErr) return rej('invalid since_id param');
 
-	// Get 'max_id' parameter
-	const [maxId, maxIdErr] = $(params.max_id).optional.id().$;
-	if (maxIdErr) return rej('invalid max_id param');
+	// Get 'until_id' parameter
+	const [untilId, untilIdErr] = $(params.until_id).optional.id().$;
+	if (untilIdErr) return rej('invalid until_id param');
 
-	// Check if both of since_id and max_id is specified
-	if (sinceId && maxId) {
-		return rej('cannot set since_id and max_id');
+	// Check if both of since_id and until_id is specified
+	if (sinceId && untilId) {
+		return rej('cannot set since_id and until_id');
 	}
 
 	const query = {
@@ -72,9 +72,9 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 		query._id = {
 			$gt: sinceId
 		};
-	} else if (maxId) {
+	} else if (untilId) {
 		query._id = {
-			$lt: maxId
+			$lt: untilId
 		};
 	}
 
diff --git a/src/api/endpoints/posts.ts b/src/api/endpoints/posts.ts
index f6efcc108..db166cd67 100644
--- a/src/api/endpoints/posts.ts
+++ b/src/api/endpoints/posts.ts
@@ -36,13 +36,13 @@ module.exports = (params) => new Promise(async (res, rej) => {
 	const [sinceId, sinceIdErr] = $(params.since_id).optional.id().$;
 	if (sinceIdErr) return rej('invalid since_id param');
 
-	// Get 'max_id' parameter
-	const [maxId, maxIdErr] = $(params.max_id).optional.id().$;
-	if (maxIdErr) return rej('invalid max_id param');
+	// Get 'until_id' parameter
+	const [untilId, untilIdErr] = $(params.until_id).optional.id().$;
+	if (untilIdErr) return rej('invalid until_id param');
 
-	// Check if both of since_id and max_id is specified
-	if (sinceId && maxId) {
-		return rej('cannot set since_id and max_id');
+	// Check if both of since_id and until_id is specified
+	if (sinceId && untilId) {
+		return rej('cannot set since_id and until_id');
 	}
 
 	// Construct query
@@ -55,9 +55,9 @@ module.exports = (params) => new Promise(async (res, rej) => {
 		query._id = {
 			$gt: sinceId
 		};
-	} else if (maxId) {
+	} else if (untilId) {
 		query._id = {
-			$lt: maxId
+			$lt: untilId
 		};
 	}
 
diff --git a/src/api/endpoints/posts/mentions.ts b/src/api/endpoints/posts/mentions.ts
index 0ebe8be50..3bb4ec3fa 100644
--- a/src/api/endpoints/posts/mentions.ts
+++ b/src/api/endpoints/posts/mentions.ts
@@ -27,13 +27,13 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 	const [sinceId, sinceIdErr] = $(params.since_id).optional.id().$;
 	if (sinceIdErr) return rej('invalid since_id param');
 
-	// Get 'max_id' parameter
-	const [maxId, maxIdErr] = $(params.max_id).optional.id().$;
-	if (maxIdErr) return rej('invalid max_id param');
+	// Get 'until_id' parameter
+	const [untilId, untilIdErr] = $(params.until_id).optional.id().$;
+	if (untilIdErr) return rej('invalid until_id param');
 
-	// Check if both of since_id and max_id is specified
-	if (sinceId && maxId) {
-		return rej('cannot set since_id and max_id');
+	// Check if both of since_id and until_id is specified
+	if (sinceId && untilId) {
+		return rej('cannot set since_id and until_id');
 	}
 
 	// Construct query
@@ -58,9 +58,9 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 		query._id = {
 			$gt: sinceId
 		};
-	} else if (maxId) {
+	} else if (untilId) {
 		query._id = {
-			$lt: maxId
+			$lt: untilId
 		};
 	}
 
diff --git a/src/api/endpoints/posts/reposts.ts b/src/api/endpoints/posts/reposts.ts
index b701ff757..bcc6163a1 100644
--- a/src/api/endpoints/posts/reposts.ts
+++ b/src/api/endpoints/posts/reposts.ts
@@ -25,13 +25,13 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 	const [sinceId, sinceIdErr] = $(params.since_id).optional.id().$;
 	if (sinceIdErr) return rej('invalid since_id param');
 
-	// Get 'max_id' parameter
-	const [maxId, maxIdErr] = $(params.max_id).optional.id().$;
-	if (maxIdErr) return rej('invalid max_id param');
+	// Get 'until_id' parameter
+	const [untilId, untilIdErr] = $(params.until_id).optional.id().$;
+	if (untilIdErr) return rej('invalid until_id param');
 
-	// Check if both of since_id and max_id is specified
-	if (sinceId && maxId) {
-		return rej('cannot set since_id and max_id');
+	// Check if both of since_id and until_id is specified
+	if (sinceId && untilId) {
+		return rej('cannot set since_id and until_id');
 	}
 
 	// Lookup post
@@ -55,9 +55,9 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 		query._id = {
 			$gt: sinceId
 		};
-	} else if (maxId) {
+	} else if (untilId) {
 		query._id = {
-			$lt: maxId
+			$lt: untilId
 		};
 	}
 
diff --git a/src/api/endpoints/posts/timeline.ts b/src/api/endpoints/posts/timeline.ts
index 0d08b9546..91cba0a04 100644
--- a/src/api/endpoints/posts/timeline.ts
+++ b/src/api/endpoints/posts/timeline.ts
@@ -25,21 +25,21 @@ module.exports = async (params, user, app) => {
 	const [sinceId, sinceIdErr] = $(params.since_id).optional.id().$;
 	if (sinceIdErr) throw 'invalid since_id param';
 
-	// Get 'max_id' parameter
-	const [maxId, maxIdErr] = $(params.max_id).optional.id().$;
-	if (maxIdErr) throw 'invalid max_id param';
+	// Get 'until_id' parameter
+	const [untilId, untilIdErr] = $(params.until_id).optional.id().$;
+	if (untilIdErr) throw 'invalid until_id param';
 
 	// Get 'since_date' parameter
 	const [sinceDate, sinceDateErr] = $(params.since_date).optional.number().$;
 	if (sinceDateErr) throw 'invalid since_date param';
 
-	// Get 'max_date' parameter
-	const [maxDate, maxDateErr] = $(params.max_date).optional.number().$;
-	if (maxDateErr) throw 'invalid max_date param';
+	// Get 'until_date' parameter
+	const [untilDate, untilDateErr] = $(params.until_date).optional.number().$;
+	if (untilDateErr) throw 'invalid until_date param';
 
-	// Check if only one of since_id, max_id, since_date, max_date specified
-	if ([sinceId, maxId, sinceDate, maxDate].filter(x => x != null).length > 1) {
-		throw 'only one of since_id, max_id, since_date, max_date can be specified';
+	// Check if only one of since_id, until_id, since_date, until_date specified
+	if ([sinceId, untilId, sinceDate, untilDate].filter(x => x != null).length > 1) {
+		throw 'only one of since_id, until_id, since_date, until_date can be specified';
 	}
 
 	const { followingIds, watchingChannelIds } = await rap({
@@ -85,18 +85,18 @@ module.exports = async (params, user, app) => {
 		query._id = {
 			$gt: sinceId
 		};
-	} else if (maxId) {
+	} else if (untilId) {
 		query._id = {
-			$lt: maxId
+			$lt: untilId
 		};
 	} else if (sinceDate) {
 		sort._id = 1;
 		query.created_at = {
 			$gt: new Date(sinceDate)
 		};
-	} else if (maxDate) {
+	} else if (untilDate) {
 		query.created_at = {
-			$lt: new Date(maxDate)
+			$lt: new Date(untilDate)
 		};
 	}
 	//#endregion
diff --git a/src/api/endpoints/users.ts b/src/api/endpoints/users.ts
index 134f262fb..f3c9b66a5 100644
--- a/src/api/endpoints/users.ts
+++ b/src/api/endpoints/users.ts
@@ -21,13 +21,13 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
 	const [sinceId, sinceIdErr] = $(params.since_id).optional.id().$;
 	if (sinceIdErr) return rej('invalid since_id param');
 
-	// Get 'max_id' parameter
-	const [maxId, maxIdErr] = $(params.max_id).optional.id().$;
-	if (maxIdErr) return rej('invalid max_id param');
+	// Get 'until_id' parameter
+	const [untilId, untilIdErr] = $(params.until_id).optional.id().$;
+	if (untilIdErr) return rej('invalid until_id param');
 
-	// Check if both of since_id and max_id is specified
-	if (sinceId && maxId) {
-		return rej('cannot set since_id and max_id');
+	// Check if both of since_id and until_id is specified
+	if (sinceId && untilId) {
+		return rej('cannot set since_id and until_id');
 	}
 
 	// Construct query
@@ -40,9 +40,9 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
 		query._id = {
 			$gt: sinceId
 		};
-	} else if (maxId) {
+	} else if (untilId) {
 		query._id = {
-			$lt: maxId
+			$lt: untilId
 		};
 	}
 
diff --git a/src/api/endpoints/users/posts.ts b/src/api/endpoints/users/posts.ts
index fe821cf17..0d8384a43 100644
--- a/src/api/endpoints/users/posts.ts
+++ b/src/api/endpoints/users/posts.ts
@@ -42,21 +42,21 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
 	const [sinceId, sinceIdErr] = $(params.since_id).optional.id().$;
 	if (sinceIdErr) return rej('invalid since_id param');
 
-	// Get 'max_id' parameter
-	const [maxId, maxIdErr] = $(params.max_id).optional.id().$;
-	if (maxIdErr) return rej('invalid max_id param');
+	// Get 'until_id' parameter
+	const [untilId, untilIdErr] = $(params.until_id).optional.id().$;
+	if (untilIdErr) return rej('invalid until_id param');
 
 	// Get 'since_date' parameter
 	const [sinceDate, sinceDateErr] = $(params.since_date).optional.number().$;
 	if (sinceDateErr) throw 'invalid since_date param';
 
-	// Get 'max_date' parameter
-	const [maxDate, maxDateErr] = $(params.max_date).optional.number().$;
-	if (maxDateErr) throw 'invalid max_date param';
+	// Get 'until_date' parameter
+	const [untilDate, untilDateErr] = $(params.until_date).optional.number().$;
+	if (untilDateErr) throw 'invalid until_date param';
 
-	// Check if only one of since_id, max_id, since_date, max_date specified
-	if ([sinceId, maxId, sinceDate, maxDate].filter(x => x != null).length > 1) {
-		throw 'only one of since_id, max_id, since_date, max_date can be specified';
+	// Check if only one of since_id, until_id, since_date, until_date specified
+	if ([sinceId, untilId, sinceDate, untilDate].filter(x => x != null).length > 1) {
+		throw 'only one of since_id, until_id, since_date, until_date can be specified';
 	}
 
 	const q = userId !== undefined
@@ -88,18 +88,18 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
 		query._id = {
 			$gt: sinceId
 		};
-	} else if (maxId) {
+	} else if (untilId) {
 		query._id = {
-			$lt: maxId
+			$lt: untilId
 		};
 	} else if (sinceDate) {
 		sort._id = 1;
 		query.created_at = {
 			$gt: new Date(sinceDate)
 		};
-	} else if (maxDate) {
+	} else if (untilDate) {
 		query.created_at = {
-			$lt: new Date(maxDate)
+			$lt: new Date(untilDate)
 		};
 	}
 
diff --git a/src/web/app/common/tags/messaging/room.tag b/src/web/app/common/tags/messaging/room.tag
index a149e1de2..7b4d1be56 100644
--- a/src/web/app/common/tags/messaging/room.tag
+++ b/src/web/app/common/tags/messaging/room.tag
@@ -254,7 +254,7 @@
 			this.api('messaging/messages', {
 				user_id: this.user.id,
 				limit: max + 1,
-				max_id: this.moreMessagesIsInStock ? this.messages[0].id : undefined
+				until_id: this.moreMessagesIsInStock ? this.messages[0].id : undefined
 			}).then(messages => {
 				if (messages.length == max + 1) {
 					this.moreMessagesIsInStock = true;
diff --git a/src/web/app/desktop/tags/home-widgets/mentions.tag b/src/web/app/desktop/tags/home-widgets/mentions.tag
index a48c7239a..268728307 100644
--- a/src/web/app/desktop/tags/home-widgets/mentions.tag
+++ b/src/web/app/desktop/tags/home-widgets/mentions.tag
@@ -101,7 +101,7 @@
 			});
 			this.api('posts/mentions', {
 				following: this.mode == 'following',
-				max_id: this.refs.timeline.tail().id
+				until_id: this.refs.timeline.tail().id
 			}).then(posts => {
 				this.update({
 					moreLoading: false
diff --git a/src/web/app/desktop/tags/home-widgets/timeline.tag b/src/web/app/desktop/tags/home-widgets/timeline.tag
index 4c58aa4aa..9571b09f3 100644
--- a/src/web/app/desktop/tags/home-widgets/timeline.tag
+++ b/src/web/app/desktop/tags/home-widgets/timeline.tag
@@ -86,7 +86,7 @@
 			});
 
 			this.api('posts/timeline', {
-				max_date: this.date ? this.date.getTime() : undefined
+				until_date: this.date ? this.date.getTime() : undefined
 			}).then(posts => {
 				this.update({
 					isLoading: false,
@@ -103,7 +103,7 @@
 				moreLoading: true
 			});
 			this.api('posts/timeline', {
-				max_id: this.refs.timeline.tail().id
+				until_id: this.refs.timeline.tail().id
 			}).then(posts => {
 				this.update({
 					moreLoading: false
diff --git a/src/web/app/desktop/tags/notifications.tag b/src/web/app/desktop/tags/notifications.tag
index 3218c00f6..39862487e 100644
--- a/src/web/app/desktop/tags/notifications.tag
+++ b/src/web/app/desktop/tags/notifications.tag
@@ -283,7 +283,7 @@
 
 			this.api('i/notifications', {
 				limit: max + 1,
-				max_id: this.notifications[this.notifications.length - 1].id
+				until_id: this.notifications[this.notifications.length - 1].id
 			}).then(notifications => {
 				if (notifications.length == max + 1) {
 					this.moreNotifications = true;
diff --git a/src/web/app/desktop/tags/user-timeline.tag b/src/web/app/desktop/tags/user-timeline.tag
index 2b05f6b5c..134aeee28 100644
--- a/src/web/app/desktop/tags/user-timeline.tag
+++ b/src/web/app/desktop/tags/user-timeline.tag
@@ -96,7 +96,7 @@
 		this.fetch = cb => {
 			this.api('users/posts', {
 				user_id: this.user.id,
-				max_date: this.date ? this.date.getTime() : undefined,
+				until_date: this.date ? this.date.getTime() : undefined,
 				with_replies: this.mode == 'with-replies'
 			}).then(posts => {
 				this.update({
@@ -116,7 +116,7 @@
 			this.api('users/posts', {
 				user_id: this.user.id,
 				with_replies: this.mode == 'with-replies',
-				max_id: this.refs.timeline.tail().id
+				until_id: this.refs.timeline.tail().id
 			}).then(posts => {
 				this.update({
 					moreLoading: false
diff --git a/src/web/app/mobile/tags/drive.tag b/src/web/app/mobile/tags/drive.tag
index 41dbfddae..2a3ff23bf 100644
--- a/src/web/app/mobile/tags/drive.tag
+++ b/src/web/app/mobile/tags/drive.tag
@@ -430,7 +430,7 @@
 			this.api('drive/files', {
 				folder_id: this.folder ? this.folder.id : null,
 				limit: max + 1,
-				max_id: this.files[this.files.length - 1].id
+				until_id: this.files[this.files.length - 1].id
 			}).then(files => {
 				if (files.length == max + 1) {
 					this.moreFiles = true;
diff --git a/src/web/app/mobile/tags/home-timeline.tag b/src/web/app/mobile/tags/home-timeline.tag
index e96823fa1..397d2b398 100644
--- a/src/web/app/mobile/tags/home-timeline.tag
+++ b/src/web/app/mobile/tags/home-timeline.tag
@@ -47,7 +47,7 @@
 
 		this.more = () => {
 			return this.api('posts/timeline', {
-				max_id: this.refs.timeline.tail().id
+				until_id: this.refs.timeline.tail().id
 			});
 		};
 
diff --git a/src/web/app/mobile/tags/notifications.tag b/src/web/app/mobile/tags/notifications.tag
index c3500d1b8..742cc4514 100644
--- a/src/web/app/mobile/tags/notifications.tag
+++ b/src/web/app/mobile/tags/notifications.tag
@@ -146,7 +146,7 @@
 
 			this.api('i/notifications', {
 				limit: max + 1,
-				max_id: this.notifications[this.notifications.length - 1].id
+				until_id: this.notifications[this.notifications.length - 1].id
 			}).then(notifications => {
 				if (notifications.length == max + 1) {
 					this.moreNotifications = true;
diff --git a/src/web/app/mobile/tags/user-timeline.tag b/src/web/app/mobile/tags/user-timeline.tag
index 4dbe719f5..86ead5971 100644
--- a/src/web/app/mobile/tags/user-timeline.tag
+++ b/src/web/app/mobile/tags/user-timeline.tag
@@ -26,7 +26,7 @@
 			return this.api('users/posts', {
 				user_id: this.user.id,
 				with_media: this.withMedia,
-				max_id: this.refs.timeline.tail().id
+				until_id: this.refs.timeline.tail().id
 			});
 		};
 	</script>
diff --git a/src/web/docs/api/endpoints/posts/timeline.yaml b/src/web/docs/api/endpoints/posts/timeline.yaml
index e1d78c082..01976b061 100644
--- a/src/web/docs/api/endpoints/posts/timeline.yaml
+++ b/src/web/docs/api/endpoints/posts/timeline.yaml
@@ -15,7 +15,7 @@ params:
     optional: true
     desc:
       ja: "指定すると、この投稿を基点としてより新しい投稿を取得します"
-  - name: "max_id"
+  - name: "until_id"
     type: "id(Post)"
     optional: true
     desc:
@@ -25,7 +25,7 @@ params:
     optional: true
     desc:
       ja: "指定した時間を基点としてより新しい投稿を取得します。数値は、1970 年 1 月 1 日 00:00:00 UTC から指定した日時までの経過時間をミリ秒単位で表します。"
-  - name: "max_date"
+  - name: "until_date"
     type: "number"
     optional: true
     desc:

From eaf0d5e637e0fd5be62b7ccf940ba1bcebeba786 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 21 Dec 2017 04:01:44 +0900
Subject: [PATCH 115/186] #1017 #155

---
 src/api/endpoints/posts/search.ts             | 96 ++++++++++++++++---
 .../app/common/scripts/parse-search-query.ts  | 41 ++++++++
 src/web/app/desktop/router.ts                 |  4 +-
 src/web/app/desktop/tags/search-posts.tag     |  6 +-
 src/web/app/desktop/tags/ui.tag               |  2 +-
 src/web/app/mobile/router.ts                  |  4 +-
 src/web/app/mobile/tags/search-posts.tag      |  6 +-
 src/web/app/mobile/tags/ui.tag                |  2 +-
 src/web/docs/search.ja.pug                    | 38 ++++++++
 9 files changed, 172 insertions(+), 27 deletions(-)
 create mode 100644 src/web/app/common/scripts/parse-search-query.ts
 create mode 100644 src/web/docs/search.ja.pug

diff --git a/src/api/endpoints/posts/search.ts b/src/api/endpoints/posts/search.ts
index b434f6434..dba7a53b5 100644
--- a/src/api/endpoints/posts/search.ts
+++ b/src/api/endpoints/posts/search.ts
@@ -5,6 +5,7 @@ import * as mongo from 'mongodb';
 import $ from 'cafy';
 const escapeRegexp = require('escape-regexp');
 import Post from '../../models/post';
+import User from '../../models/user';
 import serialize from '../../serializers/post';
 import config from '../../../conf';
 
@@ -16,33 +17,98 @@ import config from '../../../conf';
  * @return {Promise<any>}
  */
 module.exports = (params, me) => new Promise(async (res, rej) => {
-	// Get 'query' parameter
-	const [query, queryError] = $(params.query).string().pipe(x => x != '').$;
-	if (queryError) return rej('invalid query param');
+	// Get 'text' parameter
+	const [text, textError] = $(params.text).optional.string().$;
+	if (textError) return rej('invalid text param');
+
+	// Get 'user_id' parameter
+	const [userId, userIdErr] = $(params.user_id).optional.id().$;
+	if (userIdErr) return rej('invalid user_id param');
+
+	// Get 'username' parameter
+	const [username, usernameErr] = $(params.username).optional.string().$;
+	if (usernameErr) return rej('invalid username param');
+
+	// Get 'include_replies' parameter
+	const [includeReplies = true, includeRepliesErr] = $(params.include_replies).optional.boolean().$;
+	if (includeRepliesErr) return rej('invalid include_replies param');
+
+	// Get 'with_media' parameter
+	const [withMedia = false, withMediaErr] = $(params.with_media).optional.boolean().$;
+	if (withMediaErr) return rej('invalid with_media param');
+
+	// Get 'since_date' parameter
+	const [sinceDate, sinceDateErr] = $(params.since_date).optional.number().$;
+	if (sinceDateErr) throw 'invalid since_date param';
+
+	// Get 'until_date' parameter
+	const [untilDate, untilDateErr] = $(params.until_date).optional.number().$;
+	if (untilDateErr) throw 'invalid until_date param';
 
 	// Get 'offset' parameter
 	const [offset = 0, offsetErr] = $(params.offset).optional.number().min(0).$;
 	if (offsetErr) return rej('invalid offset param');
 
-	// Get 'max' parameter
-	const [max = 10, maxErr] = $(params.max).optional.number().range(1, 30).$;
-	if (maxErr) return rej('invalid max param');
+	// Get 'limit' parameter
+	const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 30).$;
+	if (limitErr) return rej('invalid limit param');
 
-	// If Elasticsearch is available, search by $
+	let user = userId;
+
+	if (user == null && username != null) {
+		const _user = await User.findOne({
+			username_lower: username.toLowerCase()
+		});
+		if (_user) {
+			user = _user._id;
+		}
+	}
+
+	// If Elasticsearch is available, search by it
 	// If not, search by MongoDB
 	(config.elasticsearch.enable ? byElasticsearch : byNative)
-		(res, rej, me, query, offset, max);
+		(res, rej, me, text, user, includeReplies, withMedia, sinceDate, untilDate, offset, limit);
 });
 
 // Search by MongoDB
-async function byNative(res, rej, me, query, offset, max) {
-	const escapedQuery = escapeRegexp(query);
+async function byNative(res, rej, me, text, userId, includeReplies, withMedia, sinceDate, untilDate, offset, max) {
+	const q: any = {};
+
+	if (text) {
+		q.$and = text.split(' ').map(x => ({
+			text: new RegExp(escapeRegexp(x))
+		}));
+	}
+
+	if (userId) {
+		q.user_id = userId;
+	}
+
+	if (!includeReplies) {
+		q.reply_id = null;
+	}
+
+	if (withMedia) {
+		q.media_ids = {
+			$exists: true,
+			$ne: null
+		};
+	}
+
+	if (sinceDate) {
+		q.created_at = {
+			$gt: new Date(sinceDate)
+		};
+	}
+
+	if (untilDate) {
+		if (q.created_at == undefined) q.created_at = {};
+		q.created_at.$lt = new Date(untilDate);
+	}
 
 	// Search posts
 	const posts = await Post
-		.find({
-			text: new RegExp(escapedQuery)
-		}, {
+		.find(q, {
 			sort: {
 				_id: -1
 			},
@@ -56,7 +122,7 @@ async function byNative(res, rej, me, query, offset, max) {
 }
 
 // Search by Elasticsearch
-async function byElasticsearch(res, rej, me, query, offset, max) {
+async function byElasticsearch(res, rej, me, text, userId, includeReplies, withMedia, sinceDate, untilDate, offset, max) {
 	const es = require('../../db/elasticsearch');
 
 	es.search({
@@ -68,7 +134,7 @@ async function byElasticsearch(res, rej, me, query, offset, max) {
 			query: {
 				simple_query_string: {
 					fields: ['text'],
-					query: query,
+					query: text,
 					default_operator: 'and'
 				}
 			},
diff --git a/src/web/app/common/scripts/parse-search-query.ts b/src/web/app/common/scripts/parse-search-query.ts
new file mode 100644
index 000000000..adcbfbb8f
--- /dev/null
+++ b/src/web/app/common/scripts/parse-search-query.ts
@@ -0,0 +1,41 @@
+export default function(qs: string) {
+	const q = {
+		text: ''
+	};
+
+	qs.split(' ').forEach(x => {
+		if (/^([a-z_]+?):(.+?)$/.test(x)) {
+			const [key, value] = x.split(':');
+			switch (key) {
+				case 'user':
+					q['username'] = value;
+					break;
+				case 'reply':
+					q['include_replies'] = value == 'true';
+					break;
+				case 'media':
+					q['with_media'] = value == 'true';
+					break;
+				case 'until':
+				case 'since':
+					// YYYY-MM-DD
+					if (/^[0-9]+\-[0-9]+\-[0-9]+$/) {
+						const [yyyy, mm, dd] = value.split('-');
+						q[`${key}_date`] = (new Date(parseInt(yyyy, 10), parseInt(mm, 10) - 1, parseInt(dd, 10))).getTime();
+					}
+					break;
+				default:
+					q[key] = value;
+					break;
+			}
+		} else {
+			q.text += x + ' ';
+		}
+	});
+
+	if (q.text) {
+		q.text = q.text.trim();
+	}
+
+	return q;
+}
diff --git a/src/web/app/desktop/router.ts b/src/web/app/desktop/router.ts
index 27b63ab2e..ce68c4f2d 100644
--- a/src/web/app/desktop/router.ts
+++ b/src/web/app/desktop/router.ts
@@ -16,7 +16,7 @@ export default (mios: MiOS) => {
 	route('/i/messaging/:user',      messaging);
 	route('/i/mentions',             mentions);
 	route('/post::post',             post);
-	route('/search::query',          search);
+	route('/search',                 search);
 	route('/:user',                  user.bind(null, 'home'));
 	route('/:user/graphs',           user.bind(null, 'graphs'));
 	route('/:user/:post',            post);
@@ -47,7 +47,7 @@ export default (mios: MiOS) => {
 
 	function search(ctx) {
 		const el = document.createElement('mk-search-page');
-		el.setAttribute('query', ctx.params.query);
+		el.setAttribute('query', ctx.querystring.substr(2));
 		mount(el);
 	}
 
diff --git a/src/web/app/desktop/tags/search-posts.tag b/src/web/app/desktop/tags/search-posts.tag
index 52f765d1a..c6b24837d 100644
--- a/src/web/app/desktop/tags/search-posts.tag
+++ b/src/web/app/desktop/tags/search-posts.tag
@@ -33,6 +33,8 @@
 
 	</style>
 	<script>
+		import parse from '../../common/scripts/parse-search-query';
+
 		this.mixin('api');
 
 		this.query = this.opts.query;
@@ -45,9 +47,7 @@
 			document.addEventListener('keydown', this.onDocumentKeydown);
 			window.addEventListener('scroll', this.onScroll);
 
-			this.api('posts/search', {
-				query: this.query
-			}).then(posts => {
+			this.api('posts/search', parse(this.query)).then(posts => {
 				this.update({
 					isLoading: false,
 					isEmpty: posts.length == 0
diff --git a/src/web/app/desktop/tags/ui.tag b/src/web/app/desktop/tags/ui.tag
index 059d88528..3dfdeec01 100644
--- a/src/web/app/desktop/tags/ui.tag
+++ b/src/web/app/desktop/tags/ui.tag
@@ -180,7 +180,7 @@
 
 		this.onsubmit = e => {
 			e.preventDefault();
-			this.page('/search:' + this.refs.q.value);
+			this.page('/search?q=' + encodeURIComponent(this.refs.q.value));
 		};
 	</script>
 </mk-ui-header-search>
diff --git a/src/web/app/mobile/router.ts b/src/web/app/mobile/router.ts
index d0c6add0b..afb9aa620 100644
--- a/src/web/app/mobile/router.ts
+++ b/src/web/app/mobile/router.ts
@@ -23,7 +23,7 @@ export default (mios: MiOS) => {
 	route('/i/settings/authorized-apps', settingsAuthorizedApps);
 	route('/post/new',                   newPost);
 	route('/post::post',                 post);
-	route('/search::query',              search);
+	route('/search',                     search);
 	route('/:user',                      user.bind(null, 'overview'));
 	route('/:user/graphs',               user.bind(null, 'graphs'));
 	route('/:user/followers',            userFollowers);
@@ -83,7 +83,7 @@ export default (mios: MiOS) => {
 
 	function search(ctx) {
 		const el = document.createElement('mk-search-page');
-		el.setAttribute('query', ctx.params.query);
+		el.setAttribute('query', ctx.querystring.substr(2));
 		mount(el);
 	}
 
diff --git a/src/web/app/mobile/tags/search-posts.tag b/src/web/app/mobile/tags/search-posts.tag
index 967764bc2..023a35bf6 100644
--- a/src/web/app/mobile/tags/search-posts.tag
+++ b/src/web/app/mobile/tags/search-posts.tag
@@ -15,6 +15,8 @@
 				width calc(100% - 32px)
 	</style>
 	<script>
+		import parse from '../../common/scripts/parse-search-query';
+
 		this.mixin('api');
 
 		this.max = 30;
@@ -24,9 +26,7 @@
 		this.withMedia = this.opts.withMedia;
 
 		this.init = new Promise((res, rej) => {
-			this.api('posts/search', {
-				query: this.query
-			}).then(posts => {
+			this.api('posts/search', parse(this.query)).then(posts => {
 				res(posts);
 				this.trigger('loaded');
 			});
diff --git a/src/web/app/mobile/tags/ui.tag b/src/web/app/mobile/tags/ui.tag
index 621f89f33..77ad14530 100644
--- a/src/web/app/mobile/tags/ui.tag
+++ b/src/web/app/mobile/tags/ui.tag
@@ -413,7 +413,7 @@
 		this.search = () => {
 			const query = window.prompt('%i18n:mobile.tags.mk-ui-nav.search%');
 			if (query == null || query == '') return;
-			this.page('/search:' + query);
+			this.page('/search?q=' + encodeURIComponent(query));
 		};
 	</script>
 </mk-ui-nav>
diff --git a/src/web/docs/search.ja.pug b/src/web/docs/search.ja.pug
new file mode 100644
index 000000000..f7ec9519f
--- /dev/null
+++ b/src/web/docs/search.ja.pug
@@ -0,0 +1,38 @@
+h1 検索
+
+p 投稿を検索することができます。
+p
+	| キーワードを半角スペースで区切ると、and検索になります。
+	| 例えば、「git コミット」と検索すると、「gitで編集したファイルの特定の行だけコミットする方法がわからない」などがマッチします。
+
+section
+	h2 オプション
+	p
+		| オプションを使用して、より高度な検索をすることもできます。
+		| オプションを指定するには、「オプション名:値」という形式でクエリに含めます。
+	p 利用可能なオプション一覧です:
+
+	table
+		thead
+			tr
+				th 名前
+				th 説明
+		tbody
+			tr
+				td user
+				td ユーザー名。投稿者を限定します。
+			tr
+				td reply
+				td 返信を含めるか否か。(trueかfalse)
+			tr
+				td media
+				td メディアが添付されているか。(trueかfalse)
+			tr
+				td until
+				td 上限の日時。(YYYY-MM-DD)
+			tr
+				td since
+				td 下限の日時。(YYYY-MM-DD)
+
+	p 例えば、「@syuiloの2017年11月1日から2017年12月31日までの『Misskey』というテキストを含む返信ではない投稿」を検索したい場合、クエリは以下のようになります:
+	code user:syuilo since:2017-11-01 until:2017-12-31 reply:false Misskey

From 1b5d788c6c480937da81fba2a5af5f670c598dca Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 21 Dec 2017 04:02:46 +0900
Subject: [PATCH 116/186] v3420

---
 CHANGELOG.md | 4 ++++
 package.json | 2 +-
 2 files changed, 5 insertions(+), 1 deletion(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index bd4f2c1f1..c8687534b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,10 @@ ChangeLog (Release Notes)
 =========================
 主に notable な changes を書いていきます
 
+3420 (2017/12/21)
+-----------------
+* 検索機能を大幅に強化
+
 3415 (2017/12/19)
 -----------------
 * デザインの調整
diff --git a/package.json b/package.json
index 39922127c..6ab49852b 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
 	"name": "misskey",
 	"author": "syuilo <i@syuilo.com>",
-	"version": "0.0.3415",
+	"version": "0.0.3420",
 	"license": "MIT",
 	"description": "A miniblog-based SNS",
 	"bugs": "https://github.com/syuilo/misskey/issues",

From 59120063fe792ba0bc230749a36b1e4acf86443f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=93=E3=81=B4=E3=81=AA=E3=81=9F=E3=81=BF=E3=81=BD?=
 <syuilotan@yahoo.co.jp>
Date: Thu, 21 Dec 2017 06:31:56 +0900
Subject: [PATCH 117/186] #1023

---
 src/api/endpoints/posts/search.ts             | 21 ++++++++++++++++---
 .../app/common/scripts/parse-search-query.ts  |  3 +++
 src/web/docs/search.ja.pug                    |  3 +++
 3 files changed, 24 insertions(+), 3 deletions(-)

diff --git a/src/api/endpoints/posts/search.ts b/src/api/endpoints/posts/search.ts
index dba7a53b5..88cdd32da 100644
--- a/src/api/endpoints/posts/search.ts
+++ b/src/api/endpoints/posts/search.ts
@@ -6,6 +6,7 @@ import $ from 'cafy';
 const escapeRegexp = require('escape-regexp');
 import Post from '../../models/post';
 import User from '../../models/user';
+import getFriends from '../../common/get-friends';
 import serialize from '../../serializers/post';
 import config from '../../../conf';
 
@@ -29,6 +30,10 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
 	const [username, usernameErr] = $(params.username).optional.string().$;
 	if (usernameErr) return rej('invalid username param');
 
+	// Get 'following' parameter
+	const [following = null, followingErr] = $(params.following).optional.nullable.boolean().$;
+	if (followingErr) return rej('invalid following param');
+
 	// Get 'include_replies' parameter
 	const [includeReplies = true, includeRepliesErr] = $(params.include_replies).optional.boolean().$;
 	if (includeRepliesErr) return rej('invalid include_replies param');
@@ -67,11 +72,11 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
 	// If Elasticsearch is available, search by it
 	// If not, search by MongoDB
 	(config.elasticsearch.enable ? byElasticsearch : byNative)
-		(res, rej, me, text, user, includeReplies, withMedia, sinceDate, untilDate, offset, limit);
+		(res, rej, me, text, user, following, includeReplies, withMedia, sinceDate, untilDate, offset, limit);
 });
 
 // Search by MongoDB
-async function byNative(res, rej, me, text, userId, includeReplies, withMedia, sinceDate, untilDate, offset, max) {
+async function byNative(res, rej, me, text, userId, following, includeReplies, withMedia, sinceDate, untilDate, offset, max) {
 	const q: any = {};
 
 	if (text) {
@@ -84,6 +89,16 @@ async function byNative(res, rej, me, text, userId, includeReplies, withMedia, s
 		q.user_id = userId;
 	}
 
+	if (following != null) {
+		const ids = await getFriends(me._id, false);
+		q.user_id = {};
+		if (following) {
+			q.user_id.$in = ids;
+		} else {
+			q.user_id.$nin = ids;
+		}
+	}
+
 	if (!includeReplies) {
 		q.reply_id = null;
 	}
@@ -122,7 +137,7 @@ async function byNative(res, rej, me, text, userId, includeReplies, withMedia, s
 }
 
 // Search by Elasticsearch
-async function byElasticsearch(res, rej, me, text, userId, includeReplies, withMedia, sinceDate, untilDate, offset, max) {
+async function byElasticsearch(res, rej, me, text, userId, following, includeReplies, withMedia, sinceDate, untilDate, offset, max) {
 	const es = require('../../db/elasticsearch');
 
 	es.search({
diff --git a/src/web/app/common/scripts/parse-search-query.ts b/src/web/app/common/scripts/parse-search-query.ts
index adcbfbb8f..62b2cf51b 100644
--- a/src/web/app/common/scripts/parse-search-query.ts
+++ b/src/web/app/common/scripts/parse-search-query.ts
@@ -10,6 +10,9 @@ export default function(qs: string) {
 				case 'user':
 					q['username'] = value;
 					break;
+				case 'follow':
+					q['following'] = value == 'null' ? null : value == 'true';
+					break;
 				case 'reply':
 					q['include_replies'] = value == 'true';
 					break;
diff --git a/src/web/docs/search.ja.pug b/src/web/docs/search.ja.pug
index f7ec9519f..7d4d23fb6 100644
--- a/src/web/docs/search.ja.pug
+++ b/src/web/docs/search.ja.pug
@@ -21,6 +21,9 @@ section
 			tr
 				td user
 				td ユーザー名。投稿者を限定します。
+			tr
+				td follow
+				td フォローしているユーザーのみに限定。(trueかfalse)
 			tr
 				td reply
 				td 返信を含めるか否か。(trueかfalse)

From 143a30295d9943a9b5bc4111f995911fe6e5c042 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=93=E3=81=B4=E3=81=AA=E3=81=9F=E3=81=BF=E3=81=BD?=
 <syuilotan@yahoo.co.jp>
Date: Thu, 21 Dec 2017 06:33:03 +0900
Subject: [PATCH 118/186] v3422

---
 CHANGELOG.md | 4 ++++
 package.json | 2 +-
 2 files changed, 5 insertions(+), 1 deletion(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index c8687534b..4159d025e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,10 @@ ChangeLog (Release Notes)
 =========================
 主に notable な changes を書いていきます
 
+3422 (2017/12/21)
+-----------------
+* 検索にfollow追加 #1023
+
 3420 (2017/12/21)
 -----------------
 * 検索機能を大幅に強化
diff --git a/package.json b/package.json
index 6ab49852b..54fbd9e99 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
 	"name": "misskey",
 	"author": "syuilo <i@syuilo.com>",
-	"version": "0.0.3420",
+	"version": "0.0.3422",
 	"license": "MIT",
 	"description": "A miniblog-based SNS",
 	"bugs": "https://github.com/syuilo/misskey/issues",

From 40f5e67ff0f803fab117c405a0614df915381433 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=93=E3=81=B4=E3=81=AA=E3=81=9F=E3=81=BF=E3=81=BD?=
 <syuilotan@yahoo.co.jp>
Date: Thu, 21 Dec 2017 07:35:16 +0900
Subject: [PATCH 119/186] :v:

---
 src/api/endpoints/posts/search.ts             | 130 ++++++++++++++----
 .../app/common/scripts/parse-search-query.ts  |   7 +-
 src/web/docs/search.ja.pug                    |  29 +++-
 3 files changed, 131 insertions(+), 35 deletions(-)

diff --git a/src/api/endpoints/posts/search.ts b/src/api/endpoints/posts/search.ts
index 88cdd32da..21e9134d3 100644
--- a/src/api/endpoints/posts/search.ts
+++ b/src/api/endpoints/posts/search.ts
@@ -34,13 +34,17 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
 	const [following = null, followingErr] = $(params.following).optional.nullable.boolean().$;
 	if (followingErr) return rej('invalid following param');
 
-	// Get 'include_replies' parameter
-	const [includeReplies = true, includeRepliesErr] = $(params.include_replies).optional.boolean().$;
-	if (includeRepliesErr) return rej('invalid include_replies param');
+	// Get 'reply' parameter
+	const [reply = null, replyErr] = $(params.reply).optional.nullable.boolean().$;
+	if (replyErr) return rej('invalid reply param');
 
-	// Get 'with_media' parameter
-	const [withMedia = false, withMediaErr] = $(params.with_media).optional.boolean().$;
-	if (withMediaErr) return rej('invalid with_media param');
+	// Get 'repost' parameter
+	const [repost = null, repostErr] = $(params.repost).optional.nullable.boolean().$;
+	if (repostErr) return rej('invalid repost param');
+
+	// Get 'media' parameter
+	const [media = null, mediaErr] = $(params.media).optional.nullable.boolean().$;
+	if (mediaErr) return rej('invalid media param');
 
 	// Get 'since_date' parameter
 	const [sinceDate, sinceDateErr] = $(params.since_date).optional.number().$;
@@ -72,53 +76,119 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
 	// If Elasticsearch is available, search by it
 	// If not, search by MongoDB
 	(config.elasticsearch.enable ? byElasticsearch : byNative)
-		(res, rej, me, text, user, following, includeReplies, withMedia, sinceDate, untilDate, offset, limit);
+		(res, rej, me, text, user, following, reply, repost, media, sinceDate, untilDate, offset, limit);
 });
 
 // Search by MongoDB
-async function byNative(res, rej, me, text, userId, following, includeReplies, withMedia, sinceDate, untilDate, offset, max) {
-	const q: any = {};
+async function byNative(res, rej, me, text, userId, following, reply, repost, media, sinceDate, untilDate, offset, max) {
+	const q: any = {
+		$and: []
+	};
+
+	const push = q.$and.push;
 
 	if (text) {
-		q.$and = text.split(' ').map(x => ({
-			text: new RegExp(escapeRegexp(x))
-		}));
+		push({
+			$and: text.split(' ').map(x => ({
+				text: new RegExp(escapeRegexp(x))
+			}))
+		});
 	}
 
 	if (userId) {
-		q.user_id = userId;
+		push({
+			user_id: userId
+		});
 	}
 
 	if (following != null) {
 		const ids = await getFriends(me._id, false);
-		q.user_id = {};
-		if (following) {
-			q.user_id.$in = ids;
+		push({
+			user_id: following ? {
+				$in: ids
+			} : {
+				$nin: ids
+			}
+		});
+	}
+
+	if (reply != null) {
+		if (reply) {
+			push({
+				reply_id: {
+					$exists: true,
+					$ne: null
+				}
+			});
 		} else {
-			q.user_id.$nin = ids;
+			push({
+				$or: [{
+					reply_id: {
+						$exists: false
+					}
+				}, {
+					reply_id: null
+				}]
+			});
 		}
 	}
 
-	if (!includeReplies) {
-		q.reply_id = null;
+	if (repost != null) {
+		if (repost) {
+			push({
+				repost_id: {
+					$exists: true,
+					$ne: null
+				}
+			});
+		} else {
+			push({
+				$or: [{
+					repost_id: {
+						$exists: false
+					}
+				}, {
+					repost_id: null
+				}]
+			});
+		}
 	}
 
-	if (withMedia) {
-		q.media_ids = {
-			$exists: true,
-			$ne: null
-		};
+	if (media != null) {
+		if (media) {
+			push({
+				media_ids: {
+					$exists: true,
+					$ne: null
+				}
+			});
+		} else {
+			push({
+				$or: [{
+					media_ids: {
+						$exists: false
+					}
+				}, {
+					media_ids: null
+				}]
+			});
+		}
 	}
 
 	if (sinceDate) {
-		q.created_at = {
-			$gt: new Date(sinceDate)
-		};
+		push({
+			created_at: {
+				$gt: new Date(sinceDate)
+			}
+		});
 	}
 
 	if (untilDate) {
-		if (q.created_at == undefined) q.created_at = {};
-		q.created_at.$lt = new Date(untilDate);
+		push({
+			created_at: {
+				$lt: new Date(untilDate)
+			}
+		});
 	}
 
 	// Search posts
@@ -137,7 +207,7 @@ async function byNative(res, rej, me, text, userId, following, includeReplies, w
 }
 
 // Search by Elasticsearch
-async function byElasticsearch(res, rej, me, text, userId, following, includeReplies, withMedia, sinceDate, untilDate, offset, max) {
+async function byElasticsearch(res, rej, me, text, userId, following, reply, repost, media, sinceDate, untilDate, offset, max) {
 	const es = require('../../db/elasticsearch');
 
 	es.search({
diff --git a/src/web/app/common/scripts/parse-search-query.ts b/src/web/app/common/scripts/parse-search-query.ts
index 62b2cf51b..f65e4683a 100644
--- a/src/web/app/common/scripts/parse-search-query.ts
+++ b/src/web/app/common/scripts/parse-search-query.ts
@@ -14,10 +14,13 @@ export default function(qs: string) {
 					q['following'] = value == 'null' ? null : value == 'true';
 					break;
 				case 'reply':
-					q['include_replies'] = value == 'true';
+					q['reply'] = value == 'null' ? null : value == 'true';
+					break;
+				case 'repost':
+					q['repost'] = value == 'null' ? null : value == 'true';
 					break;
 				case 'media':
-					q['with_media'] = value == 'true';
+					q['media'] = value == 'null' ? null : value == 'true';
 					break;
 				case 'until':
 				case 'since':
diff --git a/src/web/docs/search.ja.pug b/src/web/docs/search.ja.pug
index 7d4d23fb6..d46e5f4a0 100644
--- a/src/web/docs/search.ja.pug
+++ b/src/web/docs/search.ja.pug
@@ -23,13 +23,36 @@ section
 				td ユーザー名。投稿者を限定します。
 			tr
 				td follow
-				td フォローしているユーザーのみに限定。(trueかfalse)
+				td
+					| true ... フォローしているユーザーに限定。
+					br
+					| false ... フォローしていないユーザーに限定。
+					br
+					| null ... 特に限定しない(デフォルト)
 			tr
 				td reply
-				td 返信を含めるか否か。(trueかfalse)
+				td
+					| true ... 返信に限定。
+					br
+					| false ... 返信でない投稿に限定。
+					br
+					| null ... 特に限定しない(デフォルト)
+			tr
+				td repost
+				td
+					| true ... Repostに限定。
+					br
+					| false ... Repostでない投稿に限定。
+					br
+					| null ... 特に限定しない(デフォルト)
 			tr
 				td media
-				td メディアが添付されているか。(trueかfalse)
+				td
+					| true ... メディアが添付されている投稿に限定。
+					br
+					| false ... メディアが添付されていない投稿に限定。
+					br
+					| null ... 特に限定しない(デフォルト)
 			tr
 				td until
 				td 上限の日時。(YYYY-MM-DD)

From ef08c78b8153e5bc33c0087b18acac505b8956f8 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=93=E3=81=B4=E3=81=AA=E3=81=9F=E3=81=BF=E3=81=BD?=
 <syuilotan@yahoo.co.jp>
Date: Thu, 21 Dec 2017 07:35:23 +0900
Subject: [PATCH 120/186] v3424

---
 CHANGELOG.md | 5 +++++
 package.json | 2 +-
 2 files changed, 6 insertions(+), 1 deletion(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4159d025e..c33ecddb1 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,11 @@ ChangeLog (Release Notes)
 =========================
 主に notable な changes を書いていきます
 
+3424 (2017/12/21)
+-----------------
+* 検索にrepost追加
+* など
+
 3422 (2017/12/21)
 -----------------
 * 検索にfollow追加 #1023
diff --git a/package.json b/package.json
index 54fbd9e99..a85ffded0 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
 	"name": "misskey",
 	"author": "syuilo <i@syuilo.com>",
-	"version": "0.0.3422",
+	"version": "0.0.3424",
 	"license": "MIT",
 	"description": "A miniblog-based SNS",
 	"bugs": "https://github.com/syuilo/misskey/issues",

From b2223c19e18ee5884ea13ec0d33f3ddb1ac9ea27 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=93=E3=81=B4=E3=81=AA=E3=81=9F=E3=81=BF=E3=81=BD?=
 <syuilotan@yahoo.co.jp>
Date: Thu, 21 Dec 2017 07:45:45 +0900
Subject: [PATCH 121/186] Fix bug

---
 src/api/endpoints/posts/search.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/api/endpoints/posts/search.ts b/src/api/endpoints/posts/search.ts
index 21e9134d3..a3c44d09c 100644
--- a/src/api/endpoints/posts/search.ts
+++ b/src/api/endpoints/posts/search.ts
@@ -85,7 +85,7 @@ async function byNative(res, rej, me, text, userId, following, reply, repost, me
 		$and: []
 	};
 
-	const push = q.$and.push;
+	const push = x => q.$and.push(x);
 
 	if (text) {
 		push({

From aff76a57c0d123b992d7284faba6c5a146985246 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=93=E3=81=B4=E3=81=AA=E3=81=9F=E3=81=BF=E3=81=BD?=
 <syuilotan@yahoo.co.jp>
Date: Thu, 21 Dec 2017 07:57:31 +0900
Subject: [PATCH 122/186] :v:

---
 src/api/endpoints/posts/search.ts             | 31 +++++++++++++++++--
 .../app/common/scripts/parse-search-query.ts  |  3 ++
 src/web/docs/search.ja.pug                    |  8 +++++
 3 files changed, 39 insertions(+), 3 deletions(-)

diff --git a/src/api/endpoints/posts/search.ts b/src/api/endpoints/posts/search.ts
index a3c44d09c..777cd7909 100644
--- a/src/api/endpoints/posts/search.ts
+++ b/src/api/endpoints/posts/search.ts
@@ -46,6 +46,10 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
 	const [media = null, mediaErr] = $(params.media).optional.nullable.boolean().$;
 	if (mediaErr) return rej('invalid media param');
 
+	// Get 'poll' parameter
+	const [poll = null, pollErr] = $(params.poll).optional.nullable.boolean().$;
+	if (pollErr) return rej('invalid poll param');
+
 	// Get 'since_date' parameter
 	const [sinceDate, sinceDateErr] = $(params.since_date).optional.number().$;
 	if (sinceDateErr) throw 'invalid since_date param';
@@ -76,11 +80,11 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
 	// If Elasticsearch is available, search by it
 	// If not, search by MongoDB
 	(config.elasticsearch.enable ? byElasticsearch : byNative)
-		(res, rej, me, text, user, following, reply, repost, media, sinceDate, untilDate, offset, limit);
+		(res, rej, me, text, user, following, reply, repost, media, poll, sinceDate, untilDate, offset, limit);
 });
 
 // Search by MongoDB
-async function byNative(res, rej, me, text, userId, following, reply, repost, media, sinceDate, untilDate, offset, max) {
+async function byNative(res, rej, me, text, userId, following, reply, repost, media, poll, sinceDate, untilDate, offset, max) {
 	const q: any = {
 		$and: []
 	};
@@ -175,6 +179,27 @@ async function byNative(res, rej, me, text, userId, following, reply, repost, me
 		}
 	}
 
+	if (poll != null) {
+		if (poll) {
+			push({
+				poll: {
+					$exists: true,
+					$ne: null
+				}
+			});
+		} else {
+			push({
+				$or: [{
+					poll: {
+						$exists: false
+					}
+				}, {
+					poll: null
+				}]
+			});
+		}
+	}
+
 	if (sinceDate) {
 		push({
 			created_at: {
@@ -207,7 +232,7 @@ async function byNative(res, rej, me, text, userId, following, reply, repost, me
 }
 
 // Search by Elasticsearch
-async function byElasticsearch(res, rej, me, text, userId, following, reply, repost, media, sinceDate, untilDate, offset, max) {
+async function byElasticsearch(res, rej, me, text, userId, following, reply, repost, media, poll, sinceDate, untilDate, offset, max) {
 	const es = require('../../db/elasticsearch');
 
 	es.search({
diff --git a/src/web/app/common/scripts/parse-search-query.ts b/src/web/app/common/scripts/parse-search-query.ts
index f65e4683a..c021ee641 100644
--- a/src/web/app/common/scripts/parse-search-query.ts
+++ b/src/web/app/common/scripts/parse-search-query.ts
@@ -22,6 +22,9 @@ export default function(qs: string) {
 				case 'media':
 					q['media'] = value == 'null' ? null : value == 'true';
 					break;
+				case 'poll':
+					q['poll'] = value == 'null' ? null : value == 'true';
+					break;
 				case 'until':
 				case 'since':
 					// YYYY-MM-DD
diff --git a/src/web/docs/search.ja.pug b/src/web/docs/search.ja.pug
index d46e5f4a0..41e443d74 100644
--- a/src/web/docs/search.ja.pug
+++ b/src/web/docs/search.ja.pug
@@ -53,6 +53,14 @@ section
 					| false ... メディアが添付されていない投稿に限定。
 					br
 					| null ... 特に限定しない(デフォルト)
+			tr
+				td poll
+				td
+					| true ... 投票が添付されている投稿に限定。
+					br
+					| false ... 投票が添付されていない投稿に限定。
+					br
+					| null ... 特に限定しない(デフォルト)
 			tr
 				td until
 				td 上限の日時。(YYYY-MM-DD)

From 42cfe26f19a9e3490dd33f18679433c3e80937ca Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=93=E3=81=B4=E3=81=AA=E3=81=9F=E3=81=BF=E3=81=BD?=
 <syuilotan@yahoo.co.jp>
Date: Thu, 21 Dec 2017 07:58:04 +0900
Subject: [PATCH 123/186] v3426

---
 CHANGELOG.md | 4 ++++
 package.json | 2 +-
 2 files changed, 5 insertions(+), 1 deletion(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index c33ecddb1..ff417fde9 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,10 @@ ChangeLog (Release Notes)
 =========================
 主に notable な changes を書いていきます
 
+3426 (2017/12/21)
+-----------------
+* 検索にpoll追加
+
 3424 (2017/12/21)
 -----------------
 * 検索にrepost追加
diff --git a/package.json b/package.json
index a85ffded0..d3960472d 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
 	"name": "misskey",
 	"author": "syuilo <i@syuilo.com>",
-	"version": "0.0.3424",
+	"version": "0.0.3426",
 	"license": "MIT",
 	"description": "A miniblog-based SNS",
 	"bugs": "https://github.com/syuilo/misskey/issues",

From 169142cec3e10f157b43342267e22c11f890ff29 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=93=E3=81=B4=E3=81=AA=E3=81=9F=E3=81=BF=E3=81=BD?=
 <syuilotan@yahoo.co.jp>
Date: Thu, 21 Dec 2017 11:08:17 +0900
Subject: [PATCH 124/186] Fix #1025

---
 src/web/app/desktop/tags/search-posts.tag | 13 +++++++------
 src/web/app/mobile/tags/search-posts.tag  | 10 ++++------
 2 files changed, 11 insertions(+), 12 deletions(-)

diff --git a/src/web/app/desktop/tags/search-posts.tag b/src/web/app/desktop/tags/search-posts.tag
index c6b24837d..2acb675d4 100644
--- a/src/web/app/desktop/tags/search-posts.tag
+++ b/src/web/app/desktop/tags/search-posts.tag
@@ -41,7 +41,8 @@
 		this.isLoading = true;
 		this.isEmpty = false;
 		this.moreLoading = false;
-		this.page = 0;
+		this.limit = 30;
+		this.offset = 0;
 
 		this.on('mount', () => {
 			document.addEventListener('keydown', this.onDocumentKeydown);
@@ -72,16 +73,16 @@
 
 		this.more = () => {
 			if (this.moreLoading || this.isLoading || this.timeline.posts.length == 0) return;
+			this.offset += this.limit;
 			this.update({
 				moreLoading: true
 			});
-			this.api('posts/search', {
-				query: this.query,
-				page: this.page + 1
+			return this.api('posts/search', Object.assign({}, parse(this.query), {
+				limit: this.limit,
+				offset: this.offset
 			}).then(posts => {
 				this.update({
-					moreLoading: false,
-					page: page + 1
+					moreLoading: false
 				});
 				this.refs.timeline.prependPosts(posts);
 			});
diff --git a/src/web/app/mobile/tags/search-posts.tag b/src/web/app/mobile/tags/search-posts.tag
index 023a35bf6..b37ba69e8 100644
--- a/src/web/app/mobile/tags/search-posts.tag
+++ b/src/web/app/mobile/tags/search-posts.tag
@@ -19,11 +19,10 @@
 
 		this.mixin('api');
 
-		this.max = 30;
+		this.limit = 30;
 		this.offset = 0;
 
 		this.query = this.opts.query;
-		this.withMedia = this.opts.withMedia;
 
 		this.init = new Promise((res, rej) => {
 			this.api('posts/search', parse(this.query)).then(posts => {
@@ -33,10 +32,9 @@
 		});
 
 		this.more = () => {
-			this.offset += this.max;
-			return this.api('posts/search', {
-				query: this.query,
-				max: this.max,
+			this.offset += this.limit;
+			return this.api('posts/search', Object.assign({}, parse(this.query), {
+				limit: this.limit,
 				offset: this.offset
 			});
 		};

From 72295b246414dbd29b860f84cab65bb8e137edc2 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=93=E3=81=B4=E3=81=AA=E3=81=9F=E3=81=BF=E3=81=BD?=
 <syuilotan@yahoo.co.jp>
Date: Thu, 21 Dec 2017 11:09:39 +0900
Subject: [PATCH 125/186] v3428

---
 CHANGELOG.md | 4 ++++
 package.json | 2 +-
 2 files changed, 5 insertions(+), 1 deletion(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index ff417fde9..db21a15d4 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,10 @@ ChangeLog (Release Notes)
 =========================
 主に notable な changes を書いていきます
 
+3428 (2017/12/21)
+-----------------
+* バグ修正
+
 3426 (2017/12/21)
 -----------------
 * 検索にpoll追加
diff --git a/package.json b/package.json
index d3960472d..c3dc9da45 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
 	"name": "misskey",
 	"author": "syuilo <i@syuilo.com>",
-	"version": "0.0.3426",
+	"version": "0.0.3428",
 	"license": "MIT",
 	"description": "A miniblog-based SNS",
 	"bugs": "https://github.com/syuilo/misskey/issues",

From f7d3b2c6ec8b467390bd030ddf7215a09610faad Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=93=E3=81=B4=E3=81=AA=E3=81=9F=E3=81=BF=E3=81=BD?=
 <syuilotan@yahoo.co.jp>
Date: Thu, 21 Dec 2017 11:13:49 +0900
Subject: [PATCH 126/186] oops

---
 src/web/app/desktop/tags/search-posts.tag | 2 +-
 src/web/app/mobile/tags/search-posts.tag  | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/web/app/desktop/tags/search-posts.tag b/src/web/app/desktop/tags/search-posts.tag
index 2acb675d4..f7ec85a4f 100644
--- a/src/web/app/desktop/tags/search-posts.tag
+++ b/src/web/app/desktop/tags/search-posts.tag
@@ -80,7 +80,7 @@
 			return this.api('posts/search', Object.assign({}, parse(this.query), {
 				limit: this.limit,
 				offset: this.offset
-			}).then(posts => {
+			})).then(posts => {
 				this.update({
 					moreLoading: false
 				});
diff --git a/src/web/app/mobile/tags/search-posts.tag b/src/web/app/mobile/tags/search-posts.tag
index b37ba69e8..3e3c034f2 100644
--- a/src/web/app/mobile/tags/search-posts.tag
+++ b/src/web/app/mobile/tags/search-posts.tag
@@ -36,7 +36,7 @@
 			return this.api('posts/search', Object.assign({}, parse(this.query), {
 				limit: this.limit,
 				offset: this.offset
-			});
+			}));
 		};
 	</script>
 </mk-search-posts>

From 52287878b7098c15b18cf2b75be9c526fc88cd5c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=93=E3=81=B4=E3=81=AA=E3=81=9F=E3=81=BF=E3=81=BD?=
 <syuilotan@yahoo.co.jp>
Date: Thu, 21 Dec 2017 11:14:16 +0900
Subject: [PATCH 127/186] v3430

---
 CHANGELOG.md | 4 ++++
 package.json | 2 +-
 2 files changed, 5 insertions(+), 1 deletion(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index db21a15d4..c253d1f11 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,10 @@ ChangeLog (Release Notes)
 =========================
 主に notable な changes を書いていきます
 
+3430 (2017/12/21)
+-----------------
+* oops
+
 3428 (2017/12/21)
 -----------------
 * バグ修正
diff --git a/package.json b/package.json
index c3dc9da45..b43f5be70 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
 	"name": "misskey",
 	"author": "syuilo <i@syuilo.com>",
-	"version": "0.0.3428",
+	"version": "0.0.3430",
 	"license": "MIT",
 	"description": "A miniblog-based SNS",
 	"bugs": "https://github.com/syuilo/misskey/issues",

From 0dc97370a34d88c9274f5a89f1714d7ce99f7a25 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 21 Dec 2017 15:28:50 +0900
Subject: [PATCH 128/186] Fix bug

---
 src/api/endpoints/posts/search.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/api/endpoints/posts/search.ts b/src/api/endpoints/posts/search.ts
index 777cd7909..bd3bbfc12 100644
--- a/src/api/endpoints/posts/search.ts
+++ b/src/api/endpoints/posts/search.ts
@@ -105,7 +105,7 @@ async function byNative(res, rej, me, text, userId, following, reply, repost, me
 		});
 	}
 
-	if (following != null) {
+	if (following != null && me != null) {
 		const ids = await getFriends(me._id, false);
 		push({
 			user_id: following ? {

From fd28d1bcc325ad22afc8584fc9fe6e9091d0524a Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 21 Dec 2017 15:30:15 +0900
Subject: [PATCH 129/186] #1026

---
 src/api/endpoints/posts/search.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/api/endpoints/posts/search.ts b/src/api/endpoints/posts/search.ts
index bd3bbfc12..16d54f729 100644
--- a/src/api/endpoints/posts/search.ts
+++ b/src/api/endpoints/posts/search.ts
@@ -111,7 +111,7 @@ async function byNative(res, rej, me, text, userId, following, reply, repost, me
 			user_id: following ? {
 				$in: ids
 			} : {
-				$nin: ids
+				$nin: ids.concat(me._id)
 			}
 		});
 	}

From 41623f85901437631707c308e0550b9be8e7b782 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 21 Dec 2017 15:37:26 +0900
Subject: [PATCH 130/186] Fix bug

---
 src/api/endpoints/posts/search.ts | 6 +++++-
 1 file changed, 5 insertions(+), 1 deletion(-)

diff --git a/src/api/endpoints/posts/search.ts b/src/api/endpoints/posts/search.ts
index 16d54f729..ac25652a0 100644
--- a/src/api/endpoints/posts/search.ts
+++ b/src/api/endpoints/posts/search.ts
@@ -85,7 +85,7 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
 
 // Search by MongoDB
 async function byNative(res, rej, me, text, userId, following, reply, repost, media, poll, sinceDate, untilDate, offset, max) {
-	const q: any = {
+	let q: any = {
 		$and: []
 	};
 
@@ -216,6 +216,10 @@ async function byNative(res, rej, me, text, userId, following, reply, repost, me
 		});
 	}
 
+	if (q.$and.length == 0) {
+		q = {};
+	}
+
 	// Search posts
 	const posts = await Post
 		.find(q, {

From 8fd78aebbf01755ffdecb9aa17dff1f842b194ff Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=93=E3=81=B4=E3=81=AA=E3=81=9F=E3=81=BF=E3=81=BD?=
 <syuilotan@yahoo.co.jp>
Date: Fri, 22 Dec 2017 04:50:50 +0900
Subject: [PATCH 131/186] wip

---
 src/api/endpoints/posts/timeline.ts | 12 +++++++++++-
 src/api/models/mute.ts              |  3 +++
 2 files changed, 14 insertions(+), 1 deletion(-)
 create mode 100644 src/api/models/mute.ts

diff --git a/src/api/endpoints/posts/timeline.ts b/src/api/endpoints/posts/timeline.ts
index 91cba0a04..6cc7825e6 100644
--- a/src/api/endpoints/posts/timeline.ts
+++ b/src/api/endpoints/posts/timeline.ts
@@ -77,7 +77,17 @@ module.exports = async (params, user, app) => {
 			channel_id: {
 				$in: watchingChannelIds
 			}
-		}]
+		}],
+		// mute
+		user_id: {
+			$nin: mutes
+		},
+		'_reply.user_id': {
+			$nin: mutes
+		},
+		'_repost.user_id': {
+			$nin: mutes
+		},
 	} as any;
 
 	if (sinceId) {
diff --git a/src/api/models/mute.ts b/src/api/models/mute.ts
new file mode 100644
index 000000000..16018b82f
--- /dev/null
+++ b/src/api/models/mute.ts
@@ -0,0 +1,3 @@
+import db from '../../db/mongodb';
+
+export default db.get('mute') as any; // fuck type definition

From 11e05a3a3298ea072f24ece0ad7a5c8c00bb1b23 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 22 Dec 2017 05:41:21 +0900
Subject: [PATCH 132/186] wip

---
 src/api/endpoints/posts/create.ts            |  6 +-
 tools/migration/node.2017-12-22.hiseikika.js | 67 ++++++++++++++++++++
 2 files changed, 72 insertions(+), 1 deletion(-)
 create mode 100644 tools/migration/node.2017-12-22.hiseikika.js

diff --git a/src/api/endpoints/posts/create.ts b/src/api/endpoints/posts/create.ts
index 7270efaf7..9d791538f 100644
--- a/src/api/endpoints/posts/create.ts
+++ b/src/api/endpoints/posts/create.ts
@@ -215,7 +215,11 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => {
 		poll: poll,
 		text: text,
 		user_id: user._id,
-		app_id: app ? app._id : null
+		app_id: app ? app._id : null,
+
+		// 以下非正規化データ
+		_reply: reply ? { user_id: reply.user_id } : undefined,
+		_repost: repost ? { user_id: repost.user_id } : undefined,
 	});
 
 	// Serialize
diff --git a/tools/migration/node.2017-12-22.hiseikika.js b/tools/migration/node.2017-12-22.hiseikika.js
new file mode 100644
index 000000000..ff8294c8d
--- /dev/null
+++ b/tools/migration/node.2017-12-22.hiseikika.js
@@ -0,0 +1,67 @@
+// for Node.js interpret
+
+const { default: Post } = require('../../built/api/models/post')
+const { default: zip } = require('@prezzemolo/zip')
+
+const migrate = async (post) => {
+	const x = {};
+	if (post.reply_id != null) {
+		const reply = await Post.findOne({
+			_id: post.reply_id
+		});
+		x['_reply.user_id'] = reply.user_id;
+	}
+	if (post.repost_id != null) {
+		const repost = await Post.findOne({
+			_id: post.repost_id
+		});
+		x['_repost.user_id'] = repost.user_id;
+	}
+	if (post.reply_id != null || post.repost_id != null) {
+		const result = await Post.update(post._id, {
+			$set: x,
+		});
+		return result.ok === 1;
+	} else {
+		return true;
+	}
+}
+
+async function main() {
+	const query = {
+		$or: [{
+			reply_id: {
+				$exists: true,
+				$ne: null
+			}
+		}, {
+			repost_id: {
+				$exists: true,
+				$ne: null
+			}
+		}]
+	}
+
+	const count = await Post.count(query);
+
+	const dop = Number.parseInt(process.argv[2]) || 5
+	const idop = ((count - (count % dop)) / dop) + 1
+
+	return zip(
+		1,
+		async (time) => {
+			console.log(`${time} / ${idop}`)
+			const doc = await Post.find(query, {
+				limit: dop, skip: time * dop
+			})
+			return Promise.all(doc.map(migrate))
+		},
+		idop
+	).then(a => {
+		const rv = []
+		a.forEach(e => rv.push(...e))
+		return rv
+	})
+}
+
+main().then(console.dir).catch(console.error)

From a134aa5a81ea2c443153b3723abf662a8069e36a Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 22 Dec 2017 06:03:54 +0900
Subject: [PATCH 133/186] wip

---
 src/api/endpoints.ts                | 17 +++++++
 src/api/endpoints/mute/create.ts    | 61 ++++++++++++++++++++++++
 src/api/endpoints/mute/delete.ts    | 63 +++++++++++++++++++++++++
 src/api/endpoints/mute/list.ts      | 73 +++++++++++++++++++++++++++++
 src/api/endpoints/posts/timeline.ts | 19 ++++++--
 5 files changed, 228 insertions(+), 5 deletions(-)
 create mode 100644 src/api/endpoints/mute/create.ts
 create mode 100644 src/api/endpoints/mute/delete.ts
 create mode 100644 src/api/endpoints/mute/list.ts

diff --git a/src/api/endpoints.ts b/src/api/endpoints.ts
index 1138df193..e84638157 100644
--- a/src/api/endpoints.ts
+++ b/src/api/endpoints.ts
@@ -222,6 +222,23 @@ const endpoints: Endpoint[] = [
 		withCredential: true,
 		kind: 'notification-read'
 	},
+
+	{
+		name: 'mute/create',
+		withCredential: true,
+		kind: 'account/write'
+	},
+	{
+		name: 'mute/delete',
+		withCredential: true,
+		kind: 'account/write'
+	},
+	{
+		name: 'mute/list',
+		withCredential: true,
+		kind: 'account/read'
+	},
+
 	{
 		name: 'notifications/get_unread_count',
 		withCredential: true,
diff --git a/src/api/endpoints/mute/create.ts b/src/api/endpoints/mute/create.ts
new file mode 100644
index 000000000..f44854ab5
--- /dev/null
+++ b/src/api/endpoints/mute/create.ts
@@ -0,0 +1,61 @@
+/**
+ * Module dependencies
+ */
+import $ from 'cafy';
+import User from '../../models/user';
+import Mute from '../../models/mute';
+
+/**
+ * Mute a user
+ *
+ * @param {any} params
+ * @param {any} user
+ * @return {Promise<any>}
+ */
+module.exports = (params, user) => new Promise(async (res, rej) => {
+	const muter = user;
+
+	// Get 'user_id' parameter
+	const [userId, userIdErr] = $(params.user_id).id().$;
+	if (userIdErr) return rej('invalid user_id param');
+
+	// 自分自身
+	if (user._id.equals(userId)) {
+		return rej('mutee is yourself');
+	}
+
+	// Get mutee
+	const mutee = await User.findOne({
+		_id: userId
+	}, {
+		fields: {
+			data: false,
+			profile: false
+		}
+	});
+
+	if (mutee === null) {
+		return rej('user not found');
+	}
+
+	// Check if already muting
+	const exist = await Mute.findOne({
+		muter_id: muter._id,
+		mutee_id: mutee._id,
+		deleted_at: { $exists: false }
+	});
+
+	if (exist !== null) {
+		return rej('already muting');
+	}
+
+	// Create mute
+	await Mute.insert({
+		created_at: new Date(),
+		muter_id: muter._id,
+		mutee_id: mutee._id,
+	});
+
+	// Send response
+	res();
+});
diff --git a/src/api/endpoints/mute/delete.ts b/src/api/endpoints/mute/delete.ts
new file mode 100644
index 000000000..d6bff3353
--- /dev/null
+++ b/src/api/endpoints/mute/delete.ts
@@ -0,0 +1,63 @@
+/**
+ * Module dependencies
+ */
+import $ from 'cafy';
+import User from '../../models/user';
+import Mute from '../../models/mute';
+
+/**
+ * Unmute a user
+ *
+ * @param {any} params
+ * @param {any} user
+ * @return {Promise<any>}
+ */
+module.exports = (params, user) => new Promise(async (res, rej) => {
+	const muter = user;
+
+	// Get 'user_id' parameter
+	const [userId, userIdErr] = $(params.user_id).id().$;
+	if (userIdErr) return rej('invalid user_id param');
+
+	// Check if the mutee is yourself
+	if (user._id.equals(userId)) {
+		return rej('mutee is yourself');
+	}
+
+	// Get mutee
+	const mutee = await User.findOne({
+		_id: userId
+	}, {
+		fields: {
+			data: false,
+			profile: false
+		}
+	});
+
+	if (mutee === null) {
+		return rej('user not found');
+	}
+
+	// Check not muting
+	const exist = await Mute.findOne({
+		muter_id: muter._id,
+		mutee_id: mutee._id,
+		deleted_at: { $exists: false }
+	});
+
+	if (exist === null) {
+		return rej('already not muting');
+	}
+
+	// Delete mute
+	await Mute.update({
+		_id: exist._id
+	}, {
+		$set: {
+			deleted_at: new Date()
+		}
+	});
+
+	// Send response
+	res();
+});
diff --git a/src/api/endpoints/mute/list.ts b/src/api/endpoints/mute/list.ts
new file mode 100644
index 000000000..740e19f0b
--- /dev/null
+++ b/src/api/endpoints/mute/list.ts
@@ -0,0 +1,73 @@
+/**
+ * Module dependencies
+ */
+import $ from 'cafy';
+import Mute from '../../models/mute';
+import serialize from '../../serializers/user';
+import getFriends from '../../common/get-friends';
+
+/**
+ * Get muted users of a user
+ *
+ * @param {any} params
+ * @param {any} me
+ * @return {Promise<any>}
+ */
+module.exports = (params, me) => new Promise(async (res, rej) => {
+	// Get 'iknow' parameter
+	const [iknow = false, iknowErr] = $(params.iknow).optional.boolean().$;
+	if (iknowErr) return rej('invalid iknow param');
+
+	// Get 'limit' parameter
+	const [limit = 30, limitErr] = $(params.limit).optional.number().range(1, 100).$;
+	if (limitErr) return rej('invalid limit param');
+
+	// Get 'cursor' parameter
+	const [cursor = null, cursorErr] = $(params.cursor).optional.id().$;
+	if (cursorErr) return rej('invalid cursor param');
+
+	// Construct query
+	const query = {
+		muter_id: me._id,
+		deleted_at: { $exists: false }
+	} as any;
+
+	if (iknow) {
+		// Get my friends
+		const myFriends = await getFriends(me._id);
+
+		query.mutee_id = {
+			$in: myFriends
+		};
+	}
+
+	// カーソルが指定されている場合
+	if (cursor) {
+		query._id = {
+			$lt: cursor
+		};
+	}
+
+	// Get mutes
+	const mutes = await Mute
+		.find(query, {
+			limit: limit + 1,
+			sort: { _id: -1 }
+		});
+
+	// 「次のページ」があるかどうか
+	const inStock = mutes.length === limit + 1;
+	if (inStock) {
+		mutes.pop();
+	}
+
+	// Serialize
+	const users = await Promise.all(mutes.map(async m =>
+		await serialize(m.mutee_id, me, { detail: true })));
+
+	// Response
+	res({
+		users: users,
+		next: inStock ? mutes[mutes.length - 1]._id : null,
+	});
+});
diff --git a/src/api/endpoints/posts/timeline.ts b/src/api/endpoints/posts/timeline.ts
index 6cc7825e6..da7ffd0c1 100644
--- a/src/api/endpoints/posts/timeline.ts
+++ b/src/api/endpoints/posts/timeline.ts
@@ -4,6 +4,7 @@
 import $ from 'cafy';
 import rap from '@prezzemolo/rap';
 import Post from '../../models/post';
+import Mute from '../../models/mute';
 import ChannelWatching from '../../models/channel-watching';
 import getFriends from '../../common/get-friends';
 import serialize from '../../serializers/post';
@@ -42,15 +43,23 @@ module.exports = async (params, user, app) => {
 		throw 'only one of since_id, until_id, since_date, until_date can be specified';
 	}
 
-	const { followingIds, watchingChannelIds } = await rap({
+	const { followingIds, watchingChannelIds, mutedUserIds } = await rap({
 		// ID list of the user itself and other users who the user follows
 		followingIds: getFriends(user._id),
+
 		// Watchしているチャンネルを取得
 		watchingChannelIds: ChannelWatching.find({
 			user_id: user._id,
 			// 削除されたドキュメントは除く
 			deleted_at: { $exists: false }
-		}).then(watches => watches.map(w => w.channel_id))
+		}).then(watches => watches.map(w => w.channel_id)),
+
+		// ミュートしているユーザーを取得
+		mutedUserIds: Mute.find({
+			muter_id: user._id,
+			// 削除されたドキュメントは除く
+			deleted_at: { $exists: false }
+		}).then(ms => ms.map(m => m.mutee_id))
 	});
 
 	//#region Construct query
@@ -80,13 +89,13 @@ module.exports = async (params, user, app) => {
 		}],
 		// mute
 		user_id: {
-			$nin: mutes
+			$nin: mutedUserIds
 		},
 		'_reply.user_id': {
-			$nin: mutes
+			$nin: mutedUserIds
 		},
 		'_repost.user_id': {
-			$nin: mutes
+			$nin: mutedUserIds
 		},
 	} as any;
 

From 26b40d8886ccf87eed5cce2868b14994c29752b9 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 22 Dec 2017 06:38:48 +0900
Subject: [PATCH 134/186] wip

---
 src/api/endpoints/posts/search.ts | 89 +++++++++++++++++++++++++++++--
 src/web/docs/search.ja.pug        | 16 ++++++
 2 files changed, 102 insertions(+), 3 deletions(-)

diff --git a/src/api/endpoints/posts/search.ts b/src/api/endpoints/posts/search.ts
index ac25652a0..f722231d4 100644
--- a/src/api/endpoints/posts/search.ts
+++ b/src/api/endpoints/posts/search.ts
@@ -6,6 +6,7 @@ import $ from 'cafy';
 const escapeRegexp = require('escape-regexp');
 import Post from '../../models/post';
 import User from '../../models/user';
+import Mute from '../../models/mute';
 import getFriends from '../../common/get-friends';
 import serialize from '../../serializers/post';
 import config from '../../../conf';
@@ -34,6 +35,10 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
 	const [following = null, followingErr] = $(params.following).optional.nullable.boolean().$;
 	if (followingErr) return rej('invalid following param');
 
+	// Get 'mute' parameter
+	const [mute = 'mute_all', muteErr] = $(params.mute).optional.string().$;
+	if (muteErr) return rej('invalid mute param');
+
 	// Get 'reply' parameter
 	const [reply = null, replyErr] = $(params.reply).optional.nullable.boolean().$;
 	if (replyErr) return rej('invalid reply param');
@@ -80,11 +85,11 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
 	// If Elasticsearch is available, search by it
 	// If not, search by MongoDB
 	(config.elasticsearch.enable ? byElasticsearch : byNative)
-		(res, rej, me, text, user, following, reply, repost, media, poll, sinceDate, untilDate, offset, limit);
+		(res, rej, me, text, user, following, mute, reply, repost, media, poll, sinceDate, untilDate, offset, limit);
 });
 
 // Search by MongoDB
-async function byNative(res, rej, me, text, userId, following, reply, repost, media, poll, sinceDate, untilDate, offset, max) {
+async function byNative(res, rej, me, text, userId, following, mute, reply, repost, media, poll, sinceDate, untilDate, offset, max) {
 	let q: any = {
 		$and: []
 	};
@@ -116,6 +121,84 @@ async function byNative(res, rej, me, text, userId, following, reply, repost, me
 		});
 	}
 
+	if (me != null) {
+		const mutes = await Mute.find({
+			muter_id: me._id,
+			deleted_at: { $exists: false }
+		});
+		const mutedUserIds = mutes.map(m => m.mutee_id);
+
+		switch (mute) {
+			case 'mute_all':
+				push({
+					user_id: {
+						$nin: mutedUserIds
+					},
+					'_reply.user_id': {
+						$nin: mutedUserIds
+					},
+					'_repost.user_id': {
+						$nin: mutedUserIds
+					}
+				});
+				break;
+			case 'mute_related':
+				push({
+					'_reply.user_id': {
+						$nin: mutedUserIds
+					},
+					'_repost.user_id': {
+						$nin: mutedUserIds
+					}
+				});
+				break;
+			case 'mute_direct':
+				push({
+					user_id: {
+						$nin: mutedUserIds
+					}
+				});
+				break;
+			case 'direct_only':
+				push({
+					user_id: {
+						$in: mutedUserIds
+					}
+				});
+				break;
+			case 'related_only':
+				push({
+					$or: [{
+						'_reply.user_id': {
+							$in: mutedUserIds
+						}
+					}, {
+						'_repost.user_id': {
+							$in: mutedUserIds
+						}
+					}]
+				});
+				break;
+			case 'all_only':
+				push({
+					$or: [{
+						user_id: {
+							$in: mutedUserIds
+						}
+					}, {
+						'_reply.user_id': {
+							$in: mutedUserIds
+						}
+					}, {
+						'_repost.user_id': {
+							$in: mutedUserIds
+						}
+					}]
+				});
+				break;
+		}
+	}
+
 	if (reply != null) {
 		if (reply) {
 			push({
@@ -236,7 +319,7 @@ async function byNative(res, rej, me, text, userId, following, reply, repost, me
 }
 
 // Search by Elasticsearch
-async function byElasticsearch(res, rej, me, text, userId, following, reply, repost, media, poll, sinceDate, untilDate, offset, max) {
+async function byElasticsearch(res, rej, me, text, userId, following, mute, reply, repost, media, poll, sinceDate, untilDate, offset, max) {
 	const es = require('../../db/elasticsearch');
 
 	es.search({
diff --git a/src/web/docs/search.ja.pug b/src/web/docs/search.ja.pug
index 41e443d74..552f95c60 100644
--- a/src/web/docs/search.ja.pug
+++ b/src/web/docs/search.ja.pug
@@ -29,6 +29,22 @@ section
 					| false ... フォローしていないユーザーに限定。
 					br
 					| null ... 特に限定しない(デフォルト)
+			tr
+				td mute
+				td
+					| mute_all ... ミュートしているユーザーの投稿とその投稿に対する返信やRepostを除外する(デフォルト)
+					br
+					| mute_related ... ミュートしているユーザーの投稿に対する返信やRepostだけ除外する
+					br
+					| mute_direct ... ミュートしているユーザーの投稿だけ除外する
+					br
+					| disabled ... ミュートしているユーザーの投稿とその投稿に対する返信やRepostも含める
+					br
+					| direct_only ... ミュートしているユーザーの投稿だけに限定
+					br
+					| related_only ... ミュートしているユーザーの投稿に対する返信やRepostだけに限定
+					br
+					| all_only ... ミュートしているユーザーの投稿とその投稿に対する返信やRepostに限定
 			tr
 				td reply
 				td

From 34923888c7f504b95912719e54325cb8633c8cda Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 22 Dec 2017 06:56:37 +0900
Subject: [PATCH 135/186] wip

---
 locales/en.yml                      |  5 +++++
 locales/ja.yml                      |  5 +++++
 src/api/serializers/user.ts         | 15 +++++++++++++--
 src/web/app/desktop/tags/user.tag   | 27 ++++++++++++++++++++++++++-
 src/web/docs/api/entities/user.yaml |  6 ++++++
 src/web/docs/mute.ja.pug            |  3 +++
 6 files changed, 58 insertions(+), 3 deletions(-)
 create mode 100644 src/web/docs/mute.ja.pug

diff --git a/locales/en.yml b/locales/en.yml
index 57e0c4116..dd3ee2a2a 100644
--- a/locales/en.yml
+++ b/locales/en.yml
@@ -473,6 +473,11 @@ desktop:
     mk-user:
       last-used-at: "Last used at"
 
+      follows-you: "Follows you"
+      mute: "Mute"
+      muted: "Muting"
+      unmute: "Unmute"
+
       photos:
         title: "Photos"
         loading: "Loading"
diff --git a/locales/ja.yml b/locales/ja.yml
index ee52f0716..d12eec86d 100644
--- a/locales/ja.yml
+++ b/locales/ja.yml
@@ -473,6 +473,11 @@ desktop:
     mk-user:
       last-used-at: "最終アクセス"
 
+      follows-you: "フォローされています"
+      mute: "ミュートする"
+      muted: "ミュートしています"
+      unmute: "ミュート解除"
+
       photos:
         title: "フォト"
         loading: "読み込み中"
diff --git a/src/api/serializers/user.ts b/src/api/serializers/user.ts
index fe924911c..ac157097a 100644
--- a/src/api/serializers/user.ts
+++ b/src/api/serializers/user.ts
@@ -6,6 +6,7 @@ import deepcopy = require('deepcopy');
 import { default as User, IUser } from '../models/user';
 import serializePost from './post';
 import Following from '../models/following';
+import Mute from '../models/mute';
 import getFriends from '../common/get-friends';
 import config from '../../conf';
 import rap from '@prezzemolo/rap';
@@ -113,7 +114,7 @@ export default (
 	}
 
 	if (meId && !meId.equals(_user.id)) {
-		// If the user is following
+		// Whether the user is following
 		_user.is_following = (async () => {
 			const follow = await Following.findOne({
 				follower_id: meId,
@@ -123,7 +124,7 @@ export default (
 			return follow !== null;
 		})();
 
-		// If the user is followed
+		// Whether the user is followed
 		_user.is_followed = (async () => {
 			const follow2 = await Following.findOne({
 				follower_id: _user.id,
@@ -132,6 +133,16 @@ export default (
 			});
 			return follow2 !== null;
 		})();
+
+		// Whether the user is muted
+		_user.is_muted = (async () => {
+			const mute = await Mute.findOne({
+				muter_id: meId,
+				mutee_id: _user.id,
+				deleted_at: { $exists: false }
+			});
+			return mute !== null;
+		})();
 	}
 
 	if (opts.detail) {
diff --git a/src/web/app/desktop/tags/user.tag b/src/web/app/desktop/tags/user.tag
index b4db47f9d..b29d1eaeb 100644
--- a/src/web/app/desktop/tags/user.tag
+++ b/src/web/app/desktop/tags/user.tag
@@ -226,7 +226,9 @@
 <mk-user-profile>
 	<div class="friend-form" if={ SIGNIN && I.id != user.id }>
 		<mk-big-follow-button user={ user }/>
-		<p class="followed" if={ user.is_followed }>フォローされています</p>
+		<p class="followed" if={ user.is_followed }>%i18n:desktop.tags.mk-user.follows-you%</p>
+		<p if={ user.is_muted }>%i18n:desktop.tags.mk-user.muted% <a onclick={ unmute }>%i18n:desktop.tags.mk-user.unmute%</a></p>
+		<p if={ !user.is_muted }><a onclick={ mute }>%i18n:desktop.tags.mk-user.mute%</a></p>
 	</div>
 	<div class="description" if={ user.description }>{ user.description }</div>
 	<div class="birthday" if={ user.profile.birthday }>
@@ -311,6 +313,7 @@
 		this.age = require('s-age');
 
 		this.mixin('i');
+		this.mixin('api');
 
 		this.user = this.opts.user;
 
@@ -325,6 +328,28 @@
 				user: this.user
 			});
 		};
+
+		this.mute = () => {
+			this.api('mute/create', {
+				user_id: this.user.id
+			}).then(() => {
+				this.user.is_muted = true;
+				this.update();
+			}, e => {
+				alert('error');
+			});
+		};
+
+		this.unmute = () => {
+			this.api('mute/delete', {
+				user_id: this.user.id
+			}).then(() => {
+				this.user.is_muted = false;
+				this.update();
+			}, e => {
+				alert('error');
+			});
+		};
 	</script>
 </mk-user-profile>
 
diff --git a/src/web/docs/api/entities/user.yaml b/src/web/docs/api/entities/user.yaml
index abc3f300d..e62ad84db 100644
--- a/src/web/docs/api/entities/user.yaml
+++ b/src/web/docs/api/entities/user.yaml
@@ -75,6 +75,12 @@ props:
     optional: true
     desc:
       ja: "自分がこのユーザーにフォローされているか"
+  - name: "is_muted"
+    type: "boolean"
+    optional: true
+    desc:
+      ja: "自分がこのユーザーをミュートしているか"
+      en: "Whether you muted this user"
   - name: "last_used_at"
     type: "date"
     optional: false
diff --git a/src/web/docs/mute.ja.pug b/src/web/docs/mute.ja.pug
new file mode 100644
index 000000000..4f5fad8b6
--- /dev/null
+++ b/src/web/docs/mute.ja.pug
@@ -0,0 +1,3 @@
+h1 ミュート
+
+p ユーザーをミュートすると、タイムラインや検索結果に対象のユーザーの投稿(およびそれらの投稿に対する返信やRepost)が表示されなくなります。

From 6575a6de5bbcab9a88448e4366feb77f1845a580 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 22 Dec 2017 07:26:23 +0900
Subject: [PATCH 136/186] wip

---
 src/api/endpoints/i/notifications.ts | 23 ++++++++++++++-----
 src/api/stream/home.ts               | 33 ++++++++++++++++++++++++++--
 2 files changed, 49 insertions(+), 7 deletions(-)

diff --git a/src/api/endpoints/i/notifications.ts b/src/api/endpoints/i/notifications.ts
index 48254e5e6..fb9be7f61 100644
--- a/src/api/endpoints/i/notifications.ts
+++ b/src/api/endpoints/i/notifications.ts
@@ -3,6 +3,7 @@
  */
 import $ from 'cafy';
 import Notification from '../../models/notification';
+import Mute from '../../models/mute';
 import serialize from '../../serializers/notification';
 import getFriends from '../../common/get-friends';
 import read from '../../common/read-notification';
@@ -45,8 +46,18 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 		return rej('cannot set since_id and until_id');
 	}
 
+	const mute = await Mute.find({
+		muter_id: user._id,
+		deleted_at: { $exists: false }
+	});
+
 	const query = {
-		notifiee_id: user._id
+		notifiee_id: user._id,
+		$and: [{
+			notifier_id: {
+				$nin: mute.map(m => m.mutee_id)
+			}
+		}]
 	} as any;
 
 	const sort = {
@@ -54,12 +65,14 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 	};
 
 	if (following) {
-		// ID list of the user $self and other users who the user follows
+		// ID list of the user itself and other users who the user follows
 		const followingIds = await getFriends(user._id);
 
-		query.notifier_id = {
-			$in: followingIds
-		};
+		query.$and.push({
+			notifier_id: {
+				$in: followingIds
+			}
+		});
 	}
 
 	if (type) {
diff --git a/src/api/stream/home.ts b/src/api/stream/home.ts
index 7c8f3bfec..7dcdb5ed7 100644
--- a/src/api/stream/home.ts
+++ b/src/api/stream/home.ts
@@ -3,19 +3,48 @@ import * as redis from 'redis';
 import * as debug from 'debug';
 
 import User from '../models/user';
+import Mute from '../models/mute';
 import serializePost from '../serializers/post';
 import readNotification from '../common/read-notification';
 
 const log = debug('misskey');
 
-export default function homeStream(request: websocket.request, connection: websocket.connection, subscriber: redis.RedisClient, user: any): void {
+export default async function(request: websocket.request, connection: websocket.connection, subscriber: redis.RedisClient, user: any) {
 	// Subscribe Home stream channel
 	subscriber.subscribe(`misskey:user-stream:${user._id}`);
 
+	const mute = await Mute.find({
+		muter_id: user._id,
+		deleted_at: { $exists: false }
+	});
+	const mutedUserIds = mute.map(m => m.mutee_id.toString());
+
 	subscriber.on('message', async (channel, data) => {
 		switch (channel.split(':')[1]) {
 			case 'user-stream':
-				connection.send(data);
+				try {
+					const x = JSON.parse(data);
+
+					if (x.type == 'post') {
+						if (mutedUserIds.indexOf(x.body.user_id) != -1) {
+							return;
+						}
+						if (x.body.reply != null && mutedUserIds.indexOf(x.body.reply.user_id) != -1) {
+							return;
+						}
+						if (x.body.repost != null && mutedUserIds.indexOf(x.body.repost.user_id) != -1) {
+							return;
+						}
+					} else if (x.type == 'notification') {
+						if (mutedUserIds.indexOf(x.body.user_id) != -1) {
+							return;
+						}
+					}
+
+					connection.send(data);
+				} catch (e) {
+					connection.send(data);
+				}
 				break;
 			case 'post-stream':
 				const postId = channel.split(':')[2];

From 8b515b4dae8c272257ee8892713fe954f0ff9c4a Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 22 Dec 2017 07:38:57 +0900
Subject: [PATCH 137/186] wip

---
 src/api/common/notify.ts                            | 12 ++++++++++++
 src/api/endpoints/notifications/get_unread_count.ts | 10 ++++++++++
 2 files changed, 22 insertions(+)

diff --git a/src/api/common/notify.ts b/src/api/common/notify.ts
index 4b3e6a5d5..f06622f91 100644
--- a/src/api/common/notify.ts
+++ b/src/api/common/notify.ts
@@ -1,5 +1,6 @@
 import * as mongo from 'mongodb';
 import Notification from '../models/notification';
+import Mute from '../models/mute';
 import event from '../event';
 import serialize from '../serializers/notification';
 
@@ -32,6 +33,17 @@ export default (
 	setTimeout(async () => {
 		const fresh = await Notification.findOne({ _id: notification._id }, { is_read: true });
 		if (!fresh.is_read) {
+			//#region ただしミュートしているユーザーからの通知なら無視
+			const mute = await Mute.find({
+				muter_id: notifiee,
+				deleted_at: { $exists: false }
+			});
+			const mutedUserIds = mute.map(m => m.mutee_id.toString());
+			if (mutedUserIds.indexOf(notifier.toHexString()) != -1) {
+				return;
+			}
+			//#endregion
+
 			event(notifiee, 'unread_notification', await serialize(notification));
 		}
 	}, 3000);
diff --git a/src/api/endpoints/notifications/get_unread_count.ts b/src/api/endpoints/notifications/get_unread_count.ts
index 9514e7871..845d6b29c 100644
--- a/src/api/endpoints/notifications/get_unread_count.ts
+++ b/src/api/endpoints/notifications/get_unread_count.ts
@@ -2,6 +2,7 @@
  * Module dependencies
  */
 import Notification from '../../models/notification';
+import Mute from '../../models/mute';
 
 /**
  * Get count of unread notifications
@@ -11,9 +12,18 @@ import Notification from '../../models/notification';
  * @return {Promise<any>}
  */
 module.exports = (params, user) => new Promise(async (res, rej) => {
+	const mute = await Mute.find({
+		muter_id: user._id,
+		deleted_at: { $exists: false }
+	});
+	const mutedUserIds = mute.map(m => m.mutee_id);
+
 	const count = await Notification
 		.count({
 			notifiee_id: user._id,
+			notifier_id: {
+				$nin: mutedUserIds
+			},
 			is_read: false
 		});
 

From f93bc3a8ec7eae63330193bc87c5b1437bf874ed Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 22 Dec 2017 07:43:56 +0900
Subject: [PATCH 138/186] wip

---
 src/api/common/notify.ts          |  2 +-
 src/api/endpoints/posts/create.ts | 14 +++++++++++---
 2 files changed, 12 insertions(+), 4 deletions(-)

diff --git a/src/api/common/notify.ts b/src/api/common/notify.ts
index f06622f91..2b79416a3 100644
--- a/src/api/common/notify.ts
+++ b/src/api/common/notify.ts
@@ -39,7 +39,7 @@ export default (
 				deleted_at: { $exists: false }
 			});
 			const mutedUserIds = mute.map(m => m.mutee_id.toString());
-			if (mutedUserIds.indexOf(notifier.toHexString()) != -1) {
+			if (mutedUserIds.indexOf(notifier.toString()) != -1) {
 				return;
 			}
 			//#endregion
diff --git a/src/api/endpoints/posts/create.ts b/src/api/endpoints/posts/create.ts
index 9d791538f..a1d05c67c 100644
--- a/src/api/endpoints/posts/create.ts
+++ b/src/api/endpoints/posts/create.ts
@@ -8,6 +8,7 @@ import { default as Post, IPost, isValidText } from '../../models/post';
 import { default as User, IUser } from '../../models/user';
 import { default as Channel, IChannel } from '../../models/channel';
 import Following from '../../models/following';
+import Mute from '../../models/mute';
 import DriveFile from '../../models/drive-file';
 import Watching from '../../models/post-watching';
 import ChannelWatching from '../../models/channel-watching';
@@ -240,7 +241,7 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => {
 
 	const mentions = [];
 
-	function addMention(mentionee, reason) {
+	async function addMention(mentionee, reason) {
 		// Reject if already added
 		if (mentions.some(x => x.equals(mentionee))) return;
 
@@ -249,8 +250,15 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => {
 
 		// Publish event
 		if (!user._id.equals(mentionee)) {
-			event(mentionee, reason, postObj);
-			pushSw(mentionee, reason, postObj);
+			const mentioneeMutes = await Mute.find({
+				muter_id: mentionee,
+				deleted_at: { $exists: false }
+			});
+			const mentioneesMutedUserIds = mentioneeMutes.map(m => m.mutee_id.toString());
+			if (mentioneesMutedUserIds.indexOf(user._id.toString()) == -1) {
+				event(mentionee, reason, postObj);
+				pushSw(mentionee, reason, postObj);
+			}
 		}
 	}
 

From 4bd694fb59f87a8adcb471dd17874d9aa65d4bc6 Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Fri, 22 Dec 2017 07:57:46 +0900
Subject: [PATCH 139/186] Update mute.ja.pug

---
 src/web/docs/mute.ja.pug | 6 +++++-
 1 file changed, 5 insertions(+), 1 deletion(-)

diff --git a/src/web/docs/mute.ja.pug b/src/web/docs/mute.ja.pug
index 4f5fad8b6..a1f396006 100644
--- a/src/web/docs/mute.ja.pug
+++ b/src/web/docs/mute.ja.pug
@@ -1,3 +1,7 @@
 h1 ミュート
 
-p ユーザーをミュートすると、タイムラインや検索結果に対象のユーザーの投稿(およびそれらの投稿に対する返信やRepost)が表示されなくなります。
+p ユーザーをミュートすると、タイムラインや検索結果に対象のユーザーの投稿(およびそれらの投稿に対する返信やRepost)が表示されなくなります。また、ミュートしているユーザーからの通知も表示されなくなります。
+
+p ユーザーページからそのユーザーをミュートすることができます。
+
+p ミュートを行ったことは相手に通知されず、ミュートされていることを知ることもできません。

From 2c4d86d8a0fca323b162adb73af60bbf644e7beb Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=93=E3=81=B4=E3=81=AA=E3=81=9F=E3=81=BF=E3=81=BD?=
 <syuilotan@yahoo.co.jp>
Date: Fri, 22 Dec 2017 10:38:59 +0900
Subject: [PATCH 140/186] wip

---
 locales/en.yml                        |  4 +++
 locales/ja.yml                        |  4 +++
 src/web/app/desktop/tags/settings.tag | 38 +++++++++++++++++++++++++++
 3 files changed, 46 insertions(+)

diff --git a/locales/en.yml b/locales/en.yml
index dd3ee2a2a..e55984677 100644
--- a/locales/en.yml
+++ b/locales/en.yml
@@ -346,6 +346,9 @@ desktop:
       failed: "Failed to setup. please ensure that the token is correct."
       info: "From the next sign in, enter the token that is displayed on the device in addition to the password."
 
+    mk-mute-setting:
+      no-users: "No muted users"
+
     mk-post-form:
       post-placeholder: "What's happening?"
       reply-placeholder: "Reply to this post..."
@@ -379,6 +382,7 @@ desktop:
 
     mk-settings:
       profile: "Profile"
+      mute: "Mute"
       drive: "Drive"
       security: "Security"
       password: "Password"
diff --git a/locales/ja.yml b/locales/ja.yml
index d12eec86d..70ff8739f 100644
--- a/locales/ja.yml
+++ b/locales/ja.yml
@@ -346,6 +346,9 @@ desktop:
       failed: "設定に失敗しました。トークンに誤りがないかご確認ください。"
       info: "次回サインインからは、同様にパスワードに加えてデバイスに表示されているトークンを入力します。"
 
+    mk-mute-setting:
+      no-users: "ミュートしているユーザーはいません"
+
     mk-post-form:
       post-placeholder: "いまどうしてる?"
       reply-placeholder: "この投稿への返信..."
@@ -379,6 +382,7 @@ desktop:
 
     mk-settings:
       profile: "プロフィール"
+      mute: "ミュート"
       drive: "ドライブ"
       security: "セキュリティ"
       password: "パスワード"
diff --git a/src/web/app/desktop/tags/settings.tag b/src/web/app/desktop/tags/settings.tag
index 2f36d9b3e..457b7e227 100644
--- a/src/web/app/desktop/tags/settings.tag
+++ b/src/web/app/desktop/tags/settings.tag
@@ -4,6 +4,7 @@
 		<p class={ active: page == 'web' } onmousedown={ setPage.bind(null, 'web') }>%fa:desktop .fw%Web</p>
 		<p class={ active: page == 'notification' } onmousedown={ setPage.bind(null, 'notification') }>%fa:R bell .fw%通知</p>
 		<p class={ active: page == 'drive' } onmousedown={ setPage.bind(null, 'drive') }>%fa:cloud .fw%%i18n:desktop.tags.mk-settings.drive%</p>
+		<p class={ active: page == 'mute' } onmousedown={ setPage.bind(null, 'mute') }>%fa:ban .fw%%i18n:desktop.tags.mk-settings.mute%</p>
 		<p class={ active: page == 'apps' } onmousedown={ setPage.bind(null, 'apps') }>%fa:puzzle-piece .fw%アプリ</p>
 		<p class={ active: page == 'twitter' } onmousedown={ setPage.bind(null, 'twitter') }>%fa:B twitter .fw%Twitter</p>
 		<p class={ active: page == 'security' } onmousedown={ setPage.bind(null, 'security') }>%fa:unlock-alt .fw%%i18n:desktop.tags.mk-settings.security%</p>
@@ -26,6 +27,11 @@
 			<mk-drive-setting/>
 		</section>
 
+		<section class="mute" show={ page == 'mute' }>
+			<h1>%i18n:desktop.tags.mk-settings.mute%</h1>
+			<mk-mute-setting/>
+		</section>
+
 		<section class="apps" show={ page == 'apps' }>
 			<h1>アプリケーション</h1>
 			<mk-authorized-apps/>
@@ -386,3 +392,35 @@
 		});
 	</script>
 </mk-drive-setting>
+
+<mk-mute-setting>
+	<div class="none ui info" if={ !fetching && users.length == 0 }>
+		<p>%fa:info-circle%%i18n:desktop.tags.mk-mute-setting.no-users%</p>
+	</div>
+	<div class="users" if={ users.length != 0 }>
+		<div each={ user in users }>
+			<p><b>{ user.name }</b> @{ user.username }</p>
+		</div>
+	</div>
+
+	<style>
+		:scope
+			display block
+
+	</style>
+	<script>
+		this.mixin('api');
+
+		this.apps = [];
+		this.fetching = true;
+
+		this.on('mount', () => {
+			this.api('mute/list').then(x => {
+				this.update({
+					fetching: false,
+					users: x.users
+				});
+			});
+		});
+	</script>
+</mk-mute-setting>

From a900843b0a7d4a4d771c903fe406ab8741a31185 Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Fri, 22 Dec 2017 12:59:37 +0900
Subject: [PATCH 141/186] Update mute.ja.pug

---
 src/web/docs/mute.ja.pug | 11 +++++++++--
 1 file changed, 9 insertions(+), 2 deletions(-)

diff --git a/src/web/docs/mute.ja.pug b/src/web/docs/mute.ja.pug
index a1f396006..176ace5e5 100644
--- a/src/web/docs/mute.ja.pug
+++ b/src/web/docs/mute.ja.pug
@@ -1,7 +1,14 @@
 h1 ミュート
 
-p ユーザーをミュートすると、タイムラインや検索結果に対象のユーザーの投稿(およびそれらの投稿に対する返信やRepost)が表示されなくなります。また、ミュートしているユーザーからの通知も表示されなくなります。
+p ユーザーページから、そのユーザーをミュートすることができます。
 
-p ユーザーページからそのユーザーをミュートすることができます。
+p ユーザーをミュートすると、そのユーザーに関する次のコンテンツがMisskeyに表示されなくなります:
+ul
+	li タイムラインや投稿の検索結果内の、そのユーザーの
+投稿(およびそれらの投稿に対する返信やRepost)
+	li そのユーザーからの通知
+	li メッセージ履歴一覧内の、そのユーザーとのメッセージ履歴
 
 p ミュートを行ったことは相手に通知されず、ミュートされていることを知ることもできません。
+
+p 設定>ミュート から、自分がミュートしているユーザー一覧を確認することができます。

From 6e59c822528dab8b011d663b1618b4ab30cd5f3d Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Fri, 22 Dec 2017 12:59:59 +0900
Subject: [PATCH 142/186] oops

---
 src/web/docs/mute.ja.pug | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)

diff --git a/src/web/docs/mute.ja.pug b/src/web/docs/mute.ja.pug
index 176ace5e5..5e79af5f8 100644
--- a/src/web/docs/mute.ja.pug
+++ b/src/web/docs/mute.ja.pug
@@ -4,8 +4,7 @@ p ユーザーページから、そのユーザーをミュートすることが
 
 p ユーザーをミュートすると、そのユーザーに関する次のコンテンツがMisskeyに表示されなくなります:
 ul
-	li タイムラインや投稿の検索結果内の、そのユーザーの
-投稿(およびそれらの投稿に対する返信やRepost)
+	li タイムラインや投稿の検索結果内の、そのユーザーの投稿(およびそれらの投稿に対する返信やRepost)
 	li そのユーザーからの通知
 	li メッセージ履歴一覧内の、そのユーザーとのメッセージ履歴
 

From e21ab77dd5fc007cc92fffd4362890fbde89db56 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 22 Dec 2017 14:21:40 +0900
Subject: [PATCH 143/186] wip

---
 src/api/endpoints/messaging/history.ts         | 11 ++++++++++-
 src/api/endpoints/messaging/messages/create.ts | 12 ++++++++++++
 2 files changed, 22 insertions(+), 1 deletion(-)

diff --git a/src/api/endpoints/messaging/history.ts b/src/api/endpoints/messaging/history.ts
index 5f7c9276d..f14740dff 100644
--- a/src/api/endpoints/messaging/history.ts
+++ b/src/api/endpoints/messaging/history.ts
@@ -3,6 +3,7 @@
  */
 import $ from 'cafy';
 import History from '../../models/messaging-history';
+import Mute from '../../models/mute';
 import serialize from '../../serializers/messaging-message';
 
 /**
@@ -17,10 +18,18 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 	const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 100).$;
 	if (limitErr) return rej('invalid limit param');
 
+	const mute = await Mute.find({
+		muter_id: user._id,
+		deleted_at: { $exists: false }
+	});
+
 	// Get history
 	const history = await History
 		.find({
-			user_id: user._id
+			user_id: user._id,
+			partner: {
+				$nin: mute.map(m => m.mutee_id)
+			}
 		}, {
 			limit: limit,
 			sort: {
diff --git a/src/api/endpoints/messaging/messages/create.ts b/src/api/endpoints/messaging/messages/create.ts
index 3c7689f96..f69f2e0fb 100644
--- a/src/api/endpoints/messaging/messages/create.ts
+++ b/src/api/endpoints/messaging/messages/create.ts
@@ -6,6 +6,7 @@ import Message from '../../../models/messaging-message';
 import { isValidText } from '../../../models/messaging-message';
 import History from '../../../models/messaging-history';
 import User from '../../../models/user';
+import Mute from '../../../models/mute';
 import DriveFile from '../../../models/drive-file';
 import serialize from '../../../serializers/messaging-message';
 import publishUserStream from '../../../event';
@@ -97,6 +98,17 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 	setTimeout(async () => {
 		const freshMessage = await Message.findOne({ _id: message._id }, { is_read: true });
 		if (!freshMessage.is_read) {
+			//#region ただしミュートしているユーザーからの通知なら無視
+			const mute = await Mute.find({
+				muter_id: recipient._id,
+				deleted_at: { $exists: false }
+			});
+			const mutedUserIds = mute.map(m => m.mutee_id.toString());
+			if (mutedUserIds.indexOf(user._id.toString()) != -1) {
+				return;
+			}
+			//#endregion
+
 			publishUserStream(message.recipient_id, 'unread_messaging_message', messageObj);
 			pushSw(message.recipient_id, 'unread_messaging_message', messageObj);
 		}

From ef5a963b32afd5842894a2a61fa4c2d078853224 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 22 Dec 2017 14:35:47 +0900
Subject: [PATCH 144/186] wip

---
 src/api/endpoints/messaging/unread.ts | 10 ++++++++++
 1 file changed, 10 insertions(+)

diff --git a/src/api/endpoints/messaging/unread.ts b/src/api/endpoints/messaging/unread.ts
index 40bc83fe1..c4326e1d2 100644
--- a/src/api/endpoints/messaging/unread.ts
+++ b/src/api/endpoints/messaging/unread.ts
@@ -2,6 +2,7 @@
  * Module dependencies
  */
 import Message from '../../models/messaging-message';
+import Mute from '../../models/mute';
 
 /**
  * Get count of unread messages
@@ -11,8 +12,17 @@ import Message from '../../models/messaging-message';
  * @return {Promise<any>}
  */
 module.exports = (params, user) => new Promise(async (res, rej) => {
+	const mute = await Mute.find({
+		muter_id: user._id,
+		deleted_at: { $exists: false }
+	});
+	const mutedUserIds = mute.map(m => m.mutee_id);
+
 	const count = await Message
 		.count({
+			user_id: {
+				$nin: mutedUserIds
+			},
 			recipient_id: user._id,
 			is_read: false
 		});

From 09af75a92d94cc59e41e0401da9af59a1a90e29f Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Fri, 22 Dec 2017 16:22:33 +0900
Subject: [PATCH 145/186] Update create.ts

---
 src/api/endpoints/messaging/messages/create.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/api/endpoints/messaging/messages/create.ts b/src/api/endpoints/messaging/messages/create.ts
index f69f2e0fb..4e9d10197 100644
--- a/src/api/endpoints/messaging/messages/create.ts
+++ b/src/api/endpoints/messaging/messages/create.ts
@@ -98,7 +98,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 	setTimeout(async () => {
 		const freshMessage = await Message.findOne({ _id: message._id }, { is_read: true });
 		if (!freshMessage.is_read) {
-			//#region ただしミュートしているユーザーからの通知なら無視
+			//#region ただしミュートされているなら発行しない
 			const mute = await Mute.find({
 				muter_id: recipient._id,
 				deleted_at: { $exists: false }

From 1ea89fddee13cd6900fbd943d3f4419450693ee5 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 22 Dec 2017 17:44:31 +0900
Subject: [PATCH 146/186] v3451

---
 CHANGELOG.md | 4 ++++
 package.json | 2 +-
 2 files changed, 5 insertions(+), 1 deletion(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index c253d1f11..bf3a52ccd 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,10 @@ ChangeLog (Release Notes)
 =========================
 主に notable な changes を書いていきます
 
+3451 (2017/12/22)
+-----------------
+* ミュート機能
+
 3430 (2017/12/21)
 -----------------
 * oops
diff --git a/package.json b/package.json
index b43f5be70..eb79e8149 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
 	"name": "misskey",
 	"author": "syuilo <i@syuilo.com>",
-	"version": "0.0.3430",
+	"version": "0.0.3451",
 	"license": "MIT",
 	"description": "A miniblog-based SNS",
 	"bugs": "https://github.com/syuilo/misskey/issues",

From 52ea44394aaf9f48340077812ea8357df157ec88 Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Sat, 23 Dec 2017 00:39:00 +0900
Subject: [PATCH 147/186] Update search.ja.pug

---
 src/web/docs/search.ja.pug | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/web/docs/search.ja.pug b/src/web/docs/search.ja.pug
index 552f95c60..e94990205 100644
--- a/src/web/docs/search.ja.pug
+++ b/src/web/docs/search.ja.pug
@@ -8,7 +8,7 @@ p
 section
 	h2 オプション
 	p
-		| オプションを使用して、より高度な検索をすることもできます。
+		| オプションを使用して、より高度な検索を行えます。
 		| オプションを指定するには、「オプション名:値」という形式でクエリに含めます。
 	p 利用可能なオプション一覧です:
 

From 8376b10b3b93d3c6cc50d69a6f0ae3ceb8c96c74 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 23 Dec 2017 04:21:15 +0900
Subject: [PATCH 148/186] #1035

---
 src/api/endpoints/posts/search.ts | 4 +++-
 src/web/docs/search.ja.pug        | 6 ++++++
 2 files changed, 9 insertions(+), 1 deletion(-)

diff --git a/src/api/endpoints/posts/search.ts b/src/api/endpoints/posts/search.ts
index f722231d4..4697e6ed0 100644
--- a/src/api/endpoints/posts/search.ts
+++ b/src/api/endpoints/posts/search.ts
@@ -99,7 +99,9 @@ async function byNative(res, rej, me, text, userId, following, mute, reply, repo
 	if (text) {
 		push({
 			$and: text.split(' ').map(x => ({
-				text: new RegExp(escapeRegexp(x))
+				text: x[0] == '-' ? {
+					$ne: new RegExp(escapeRegexp(x))
+				} : new RegExp(escapeRegexp(x))
 			}))
 		});
 	}
diff --git a/src/web/docs/search.ja.pug b/src/web/docs/search.ja.pug
index e94990205..5baac9d40 100644
--- a/src/web/docs/search.ja.pug
+++ b/src/web/docs/search.ja.pug
@@ -5,6 +5,12 @@ p
 	| キーワードを半角スペースで区切ると、and検索になります。
 	| 例えば、「git コミット」と検索すると、「gitで編集したファイルの特定の行だけコミットする方法がわからない」などがマッチします。
 
+section
+	h2 キーワードの除外
+	p キーワードの前に「-」(ハイフン)をプリフィクスすると、そのキーワードを含まない投稿に限定します。
+	p 例えば、「gitというキーワードを含むが、コミットというキーワードは含まない投稿」を検索したい場合、クエリは以下のようになります:
+	code git -コミット
+
 section
 	h2 オプション
 	p

From 67c269ddf54d43b68c06b8fb2726f72d87600c11 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 23 Dec 2017 04:21:48 +0900
Subject: [PATCH 149/186] oops

---
 src/api/endpoints/posts/search.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/api/endpoints/posts/search.ts b/src/api/endpoints/posts/search.ts
index 4697e6ed0..33ef2a0a0 100644
--- a/src/api/endpoints/posts/search.ts
+++ b/src/api/endpoints/posts/search.ts
@@ -100,7 +100,7 @@ async function byNative(res, rej, me, text, userId, following, mute, reply, repo
 		push({
 			$and: text.split(' ').map(x => ({
 				text: x[0] == '-' ? {
-					$ne: new RegExp(escapeRegexp(x))
+					$ne: new RegExp(escapeRegexp(x.substr(1)))
 				} : new RegExp(escapeRegexp(x))
 			}))
 		});

From 3e343814e395a3c4b7374629d3f6082705ea7f8d Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 23 Dec 2017 04:22:45 +0900
Subject: [PATCH 150/186] oops

---
 src/web/docs/search.ja.pug | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/web/docs/search.ja.pug b/src/web/docs/search.ja.pug
index 5baac9d40..09173a350 100644
--- a/src/web/docs/search.ja.pug
+++ b/src/web/docs/search.ja.pug
@@ -7,7 +7,7 @@ p
 
 section
 	h2 キーワードの除外
-	p キーワードの前に「-」(ハイフン)をプリフィクスすると、そのキーワードを含まない投稿に限定します。
+	p キーワードの前に「-」(ハイフン)をプリフィクスすると、そのキーワードを含まない投稿に限定します。
 	p 例えば、「gitというキーワードを含むが、コミットというキーワードは含まない投稿」を検索したい場合、クエリは以下のようになります:
 	code git -コミット
 

From 020ce794af9493d3a037bc4cd942c6f0fac75330 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 23 Dec 2017 04:26:37 +0900
Subject: [PATCH 151/186] oops

---
 src/api/endpoints/posts/search.ts | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/src/api/endpoints/posts/search.ts b/src/api/endpoints/posts/search.ts
index 33ef2a0a0..6cea5bdf5 100644
--- a/src/api/endpoints/posts/search.ts
+++ b/src/api/endpoints/posts/search.ts
@@ -99,8 +99,9 @@ async function byNative(res, rej, me, text, userId, following, mute, reply, repo
 	if (text) {
 		push({
 			$and: text.split(' ').map(x => ({
+				// キーワードが-で始まる場合そのキーワードを除外する
 				text: x[0] == '-' ? {
-					$ne: new RegExp(escapeRegexp(x.substr(1)))
+					$not: new RegExp(escapeRegexp(x.substr(1)))
 				} : new RegExp(escapeRegexp(x))
 			}))
 		});

From 7cf4aa9f110cebf1bf3017471b8e96d2522ad462 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 23 Dec 2017 04:38:56 +0900
Subject: [PATCH 152/186] #1034

---
 src/api/endpoints/posts/search.ts | 24 ++++++++++++++++--------
 src/web/docs/search.ja.pug        |  5 +++++
 2 files changed, 21 insertions(+), 8 deletions(-)

diff --git a/src/api/endpoints/posts/search.ts b/src/api/endpoints/posts/search.ts
index 6cea5bdf5..26675989d 100644
--- a/src/api/endpoints/posts/search.ts
+++ b/src/api/endpoints/posts/search.ts
@@ -97,14 +97,22 @@ async function byNative(res, rej, me, text, userId, following, mute, reply, repo
 	const push = x => q.$and.push(x);
 
 	if (text) {
-		push({
-			$and: text.split(' ').map(x => ({
-				// キーワードが-で始まる場合そのキーワードを除外する
-				text: x[0] == '-' ? {
-					$not: new RegExp(escapeRegexp(x.substr(1)))
-				} : new RegExp(escapeRegexp(x))
-			}))
-		});
+		// 完全一致検索
+		if (/"""(.+?)"""/.test(text)) {
+			const x = text.match(/"""(.+?)"""/)[1];
+			push({
+				text: x
+			});
+		} else {
+			push({
+				$and: text.split(' ').map(x => ({
+					// キーワードが-で始まる場合そのキーワードを除外する
+					text: x[0] == '-' ? {
+						$not: new RegExp(escapeRegexp(x.substr(1)))
+					} : new RegExp(escapeRegexp(x))
+				}))
+			});
+		}
 	}
 
 	if (userId) {
diff --git a/src/web/docs/search.ja.pug b/src/web/docs/search.ja.pug
index 09173a350..9e6478948 100644
--- a/src/web/docs/search.ja.pug
+++ b/src/web/docs/search.ja.pug
@@ -11,6 +11,11 @@ section
 	p 例えば、「gitというキーワードを含むが、コミットというキーワードは含まない投稿」を検索したい場合、クエリは以下のようになります:
 	code git -コミット
 
+section
+	h2 完全一致
+	p テキストを「"""」で囲むと、そのテキストと完全に一致する投稿を検索します。
+	p 例えば、「"""にゃーん"""」と検索すると、「にゃーん」という投稿のみがヒットし、「にゃーん…」という投稿はヒットしません。
+
 section
 	h2 オプション
 	p

From f0818edd6e1d566a3d7e2b5495eeb389d728f564 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 23 Dec 2017 07:21:52 +0900
Subject: [PATCH 153/186] #1037 #1038

---
 src/api/endpoints/posts/search.ts             | 138 +++++++-----------
 .../app/common/scripts/parse-search-query.ts  |   5 +-
 src/web/docs/search.ja.pug                    |  19 ++-
 3 files changed, 72 insertions(+), 90 deletions(-)

diff --git a/src/api/endpoints/posts/search.ts b/src/api/endpoints/posts/search.ts
index 26675989d..31c9a8d3c 100644
--- a/src/api/endpoints/posts/search.ts
+++ b/src/api/endpoints/posts/search.ts
@@ -1,7 +1,6 @@
 /**
  * Module dependencies
  */
-import * as mongo from 'mongodb';
 import $ from 'cafy';
 const escapeRegexp = require('escape-regexp');
 import Post from '../../models/post';
@@ -9,7 +8,6 @@ import User from '../../models/user';
 import Mute from '../../models/mute';
 import getFriends from '../../common/get-friends';
 import serialize from '../../serializers/post';
-import config from '../../../conf';
 
 /**
  * Search a post
@@ -23,13 +21,21 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
 	const [text, textError] = $(params.text).optional.string().$;
 	if (textError) return rej('invalid text param');
 
-	// Get 'user_id' parameter
-	const [userId, userIdErr] = $(params.user_id).optional.id().$;
-	if (userIdErr) return rej('invalid user_id param');
+	// Get 'include_user_ids' parameter
+	const [includeUserIds = [], includeUserIdsErr] = $(params.include_user_ids).optional.array('id').$;
+	if (includeUserIdsErr) return rej('invalid include_user_ids param');
 
-	// Get 'username' parameter
-	const [username, usernameErr] = $(params.username).optional.string().$;
-	if (usernameErr) return rej('invalid username param');
+	// Get 'exclude_user_ids' parameter
+	const [excludeUserIds = [], excludeUserIdsErr] = $(params.exclude_user_ids).optional.array('id').$;
+	if (excludeUserIdsErr) return rej('invalid exclude_user_ids param');
+
+	// Get 'include_user_usernames' parameter
+	const [includeUserUsernames = [], includeUserUsernamesErr] = $(params.include_user_usernames).optional.array('string').$;
+	if (includeUserUsernamesErr) return rej('invalid include_user_usernames param');
+
+	// Get 'exclude_user_usernames' parameter
+	const [excludeUserUsernames = [], excludeUserUsernamesErr] = $(params.exclude_user_usernames).optional.array('string').$;
+	if (excludeUserUsernamesErr) return rej('invalid exclude_user_usernames param');
 
 	// Get 'following' parameter
 	const [following = null, followingErr] = $(params.following).optional.nullable.boolean().$;
@@ -71,25 +77,36 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
 	const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 30).$;
 	if (limitErr) return rej('invalid limit param');
 
-	let user = userId;
-
-	if (user == null && username != null) {
-		const _user = await User.findOne({
-			username_lower: username.toLowerCase()
-		});
-		if (_user) {
-			user = _user._id;
-		}
+	let includeUsers = includeUserIds;
+	if (includeUserUsernames != null) {
+		const ids = (await Promise.all(includeUserUsernames.map(async (username) => {
+			const _user = await User.findOne({
+				username_lower: username.toLowerCase()
+			});
+			return _user ? _user._id : null;
+		}))).filter(id => id != null);
+		includeUsers = includeUsers.concat(ids);
 	}
 
-	// If Elasticsearch is available, search by it
-	// If not, search by MongoDB
-	(config.elasticsearch.enable ? byElasticsearch : byNative)
-		(res, rej, me, text, user, following, mute, reply, repost, media, poll, sinceDate, untilDate, offset, limit);
+	let excludeUsers = excludeUserIds;
+	if (excludeUserUsernames != null) {
+		const ids = (await Promise.all(excludeUserUsernames.map(async (username) => {
+			const _user = await User.findOne({
+				username_lower: username.toLowerCase()
+			});
+			return _user ? _user._id : null;
+		}))).filter(id => id != null);
+		excludeUsers = excludeUsers.concat(ids);
+	}
+
+	search(res, rej, me, text, includeUsers, excludeUsers, following,
+			mute, reply, repost, media, poll, sinceDate, untilDate, offset, limit);
 });
 
-// Search by MongoDB
-async function byNative(res, rej, me, text, userId, following, mute, reply, repost, media, poll, sinceDate, untilDate, offset, max) {
+async function search(
+	res, rej, me, text, includeUserIds, excludeUserIds, following,
+	mute, reply, repost, media, poll, sinceDate, untilDate, offset, max) {
+
 	let q: any = {
 		$and: []
 	};
@@ -115,9 +132,17 @@ async function byNative(res, rej, me, text, userId, following, mute, reply, repo
 		}
 	}
 
-	if (userId) {
+	if (includeUserIds && includeUserIds.length != 0) {
 		push({
-			user_id: userId
+			user_id: {
+				$in: includeUserIds
+			}
+		});
+	} else if (excludeUserIds && excludeUserIds.length != 0) {
+		push({
+			user_id: {
+				$nin: excludeUserIds
+			}
 		});
 	}
 
@@ -328,66 +353,3 @@ async function byNative(res, rej, me, text, userId, following, mute, reply, repo
 	res(await Promise.all(posts.map(async post =>
 		await serialize(post, me))));
 }
-
-// Search by Elasticsearch
-async function byElasticsearch(res, rej, me, text, userId, following, mute, reply, repost, media, poll, sinceDate, untilDate, offset, max) {
-	const es = require('../../db/elasticsearch');
-
-	es.search({
-		index: 'misskey',
-		type: 'post',
-		body: {
-			size: max,
-			from: offset,
-			query: {
-				simple_query_string: {
-					fields: ['text'],
-					query: text,
-					default_operator: 'and'
-				}
-			},
-			sort: [
-				{ _doc: 'desc' }
-			],
-			highlight: {
-				pre_tags: ['<mark>'],
-				post_tags: ['</mark>'],
-				encoder: 'html',
-				fields: {
-					text: {}
-				}
-			}
-		}
-	}, async (error, response) => {
-		if (error) {
-			console.error(error);
-			return res(500);
-		}
-
-		if (response.hits.total === 0) {
-			return res([]);
-		}
-
-		const hits = response.hits.hits.map(hit => new mongo.ObjectID(hit._id));
-
-		// Fetch found posts
-		const posts = await Post
-			.find({
-				_id: {
-					$in: hits
-				}
-			}, {
-				sort: {
-					_id: -1
-				}
-			});
-
-		posts.map(post => {
-			post._highlight = response.hits.hits.filter(hit => post._id.equals(hit._id))[0].highlight.text[0];
-		});
-
-		// Serialize
-		res(await Promise.all(posts.map(async post =>
-			await serialize(post, me))));
-	});
-}
diff --git a/src/web/app/common/scripts/parse-search-query.ts b/src/web/app/common/scripts/parse-search-query.ts
index c021ee641..512791ecb 100644
--- a/src/web/app/common/scripts/parse-search-query.ts
+++ b/src/web/app/common/scripts/parse-search-query.ts
@@ -8,7 +8,10 @@ export default function(qs: string) {
 			const [key, value] = x.split(':');
 			switch (key) {
 				case 'user':
-					q['username'] = value;
+					q['include_user_usernames'] = value.split(',');
+					break;
+				case 'exclude_user':
+					q['exclude_user_usernames'] = value.split(',');
 					break;
 				case 'follow':
 					q['following'] = value == 'null' ? null : value == 'true';
diff --git a/src/web/docs/search.ja.pug b/src/web/docs/search.ja.pug
index 9e6478948..f33091ee6 100644
--- a/src/web/docs/search.ja.pug
+++ b/src/web/docs/search.ja.pug
@@ -31,7 +31,24 @@ section
 		tbody
 			tr
 				td user
-				td ユーザー名。投稿者を限定します。
+				td
+					| 指定されたユーザー名のユーザーの投稿に限定します。
+					| 「,」(カンマ)で区切って、複数ユーザーを指定することもできます。
+					br
+					| 例えば、
+					code user:himawari,sakurako
+					| と検索すると「@himawariまたは@sakurakoの投稿」だけに限定します。
+					| (つまりユーザーのホワイトリストです)
+			tr
+				td exclude_user
+				td
+					| 指定されたユーザー名のユーザーの投稿を除外します。
+					| 「,」(カンマ)で区切って、複数ユーザーを指定することもできます。
+					br
+					| 例えば、
+					code exclude_user:akari,chinatsu
+					| と検索すると「@akariまたは@chinatsu以外の投稿」に限定します。
+					| (つまりユーザーのブラックリストです)
 			tr
 				td follow
 				td

From 84ed78cef7b84353bc1ea48c91b78ecfb2a21f08 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 23 Dec 2017 07:21:57 +0900
Subject: [PATCH 154/186] :art:

---
 src/web/docs/style.styl | 1 +
 1 file changed, 1 insertion(+)

diff --git a/src/web/docs/style.styl b/src/web/docs/style.styl
index 3dcb3e169..a726d49b1 100644
--- a/src/web/docs/style.styl
+++ b/src/web/docs/style.styl
@@ -106,6 +106,7 @@ table
 		min-width 128px
 
 code
+	display inline-block
 	padding 8px 10px
 	font-family Consolas, 'Courier New', Courier, Monaco, monospace
 	color #295c92

From f4277b8f9d1d8e3db7f720bcd0872b28162011d1 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 23 Dec 2017 07:22:58 +0900
Subject: [PATCH 155/186] v3460

---
 CHANGELOG.md | 6 ++++++
 package.json | 2 +-
 2 files changed, 7 insertions(+), 1 deletion(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index bf3a52ccd..a05097ff0 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,12 @@ ChangeLog (Release Notes)
 =========================
 主に notable な changes を書いていきます
 
+3460 (2017/12/23)
+-----------------
+* 検索で複数のユーザーを指定できるように
+* 検索でユーザーを除外できるように
+* など
+
 3451 (2017/12/22)
 -----------------
 * ミュート機能
diff --git a/package.json b/package.json
index eb79e8149..b236051e7 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
 	"name": "misskey",
 	"author": "syuilo <i@syuilo.com>",
-	"version": "0.0.3451",
+	"version": "0.0.3460",
 	"license": "MIT",
 	"description": "A miniblog-based SNS",
 	"bugs": "https://github.com/syuilo/misskey/issues",

From 8b6c30d190b0e37035c6b5c2ce5a028555f5a497 Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Sat, 23 Dec 2017 02:52:15 +0000
Subject: [PATCH 156/186] fix(package): update ts-node to version 4.1.0

Closes #985
---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index b236051e7..81588c66f 100644
--- a/package.json
+++ b/package.json
@@ -163,7 +163,7 @@
 		"tcp-port-used": "0.1.2",
 		"textarea-caret": "3.0.2",
 		"tmp": "0.0.33",
-		"ts-node": "3.3.0",
+		"ts-node": "4.1.0",
 		"tslint": "5.8.0",
 		"typescript": "2.6.2",
 		"uglify-es": "3.2.0",

From 086266cae902ff172d702357682abd8ccc16ac23 Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Sat, 23 Dec 2017 17:55:00 +0000
Subject: [PATCH 157/186] fix(package): update riot-tag-loader to version 2.0.0

Closes #1042
---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index 81588c66f..2607d33a2 100644
--- a/package.json
+++ b/package.json
@@ -146,7 +146,7 @@
 		"request": "2.83.0",
 		"rimraf": "2.6.2",
 		"riot": "3.7.4",
-		"riot-tag-loader": "1.0.0",
+		"riot-tag-loader": "2.0.0",
 		"rndstr": "1.0.0",
 		"s-age": "1.1.0",
 		"seedrandom": "2.4.3",

From eaf6e83515b219c9c54cd806060fe386caa38165 Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Sun, 24 Dec 2017 13:50:30 +0000
Subject: [PATCH 158/186] fix(package): update gulp-htmlmin to version 4.0.0

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index 2607d33a2..33ffb53b4 100644
--- a/package.json
+++ b/package.json
@@ -106,7 +106,7 @@
 		"gm": "1.23.0",
 		"gulp": "3.9.1",
 		"gulp-cssnano": "2.1.2",
-		"gulp-htmlmin": "3.0.0",
+		"gulp-htmlmin": "4.0.0",
 		"gulp-imagemin": "4.0.0",
 		"gulp-mocha": "4.3.1",
 		"gulp-pug": "3.3.0",

From 3a2e06c06c3359e48a386eca5430ab4eef6f1489 Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Sun, 24 Dec 2017 23:57:04 +0000
Subject: [PATCH 159/186] fix(package): update mongodb to version 3.0.1

Closes #1046
---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index 2607d33a2..9649e0b09 100644
--- a/package.json
+++ b/package.json
@@ -126,7 +126,7 @@
 		"mkdirp": "^0.5.1",
 		"mocha": "4.0.1",
 		"moji": "0.5.1",
-		"mongodb": "2.2.33",
+		"mongodb": "3.0.1",
 		"monk": "6.0.5",
 		"morgan": "1.9.0",
 		"ms": "2.1.1",

From 1ba3be5dbe05789bfdc08844288dd9debaeef0ca Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Mon, 25 Dec 2017 11:02:15 +0000
Subject: [PATCH 160/186] fix(package): update riot-tag-loader to version 2.0.1

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index 2607d33a2..d43e967eb 100644
--- a/package.json
+++ b/package.json
@@ -146,7 +146,7 @@
 		"request": "2.83.0",
 		"rimraf": "2.6.2",
 		"riot": "3.7.4",
-		"riot-tag-loader": "2.0.0",
+		"riot-tag-loader": "2.0.1",
 		"rndstr": "1.0.0",
 		"s-age": "1.1.0",
 		"seedrandom": "2.4.3",

From 7811853be4a85f4ec2a8364fa3f282bbf39d854d Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Wed, 27 Dec 2017 10:33:26 +0000
Subject: [PATCH 161/186] fix(package): update uglifyjs-webpack-plugin to
 version 1.1.5

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index 2607d33a2..c8ab350d4 100644
--- a/package.json
+++ b/package.json
@@ -167,7 +167,7 @@
 		"tslint": "5.8.0",
 		"typescript": "2.6.2",
 		"uglify-es": "3.2.0",
-		"uglifyjs-webpack-plugin": "1.1.4",
+		"uglifyjs-webpack-plugin": "1.1.5",
 		"uuid": "3.1.0",
 		"vhost": "3.0.2",
 		"web-push": "3.2.5",

From 9b04c7a32d60d21b87d685034fdb194ad0bdc37b Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Wed, 27 Dec 2017 17:50:23 +0000
Subject: [PATCH 162/186] fix(package): update qrcode to version 1.0.1

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index 2607d33a2..ddb18a2e7 100644
--- a/package.json
+++ b/package.json
@@ -138,7 +138,7 @@
 		"prominence": "0.2.0",
 		"proxy-addr": "2.0.2",
 		"pug": "2.0.0-rc.4",
-		"qrcode": "1.0.0",
+		"qrcode": "1.0.1",
 		"ratelimiter": "3.0.3",
 		"recaptcha-promise": "0.1.3",
 		"reconnecting-websocket": "3.2.2",

From be0d85e1b0f950921ae738e3d3b0f4c8cd027d5c Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Thu, 28 Dec 2017 09:49:30 +0900
Subject: [PATCH 163/186] Update .travis.yml

---
 .travis.yml | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/.travis.yml b/.travis.yml
index ed53af9e2..6e33a2d12 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,6 +1,9 @@
 # travis file
 # https://docs.travis-ci.com/user/customizing-the-build
 
+notifications:
+  email: false
+
 branches:
   except:
     - release

From 9ff3cccde600b56a8ae3e556521707e1b383631c Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Thu, 28 Dec 2017 04:49:49 +0000
Subject: [PATCH 164/186] fix(package): update gm to version 1.23.1

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index c3be5cdae..3fbb3f85b 100644
--- a/package.json
+++ b/package.json
@@ -103,7 +103,7 @@
 		"express": "4.16.2",
 		"file-type": "7.4.0",
 		"fuckadblock": "3.2.1",
-		"gm": "1.23.0",
+		"gm": "1.23.1",
 		"gulp": "3.9.1",
 		"gulp-cssnano": "2.1.2",
 		"gulp-htmlmin": "4.0.0",

From 113ee680246b9b969b73f4a89cdd14c1864530be Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Thu, 28 Dec 2017 14:01:33 +0000
Subject: [PATCH 165/186] fix(package): update riot to version 3.8.0

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index c3be5cdae..68d5c2726 100644
--- a/package.json
+++ b/package.json
@@ -145,7 +145,7 @@
 		"redis": "2.8.0",
 		"request": "2.83.0",
 		"rimraf": "2.6.2",
-		"riot": "3.7.4",
+		"riot": "3.8.0",
 		"riot-tag-loader": "2.0.1",
 		"rndstr": "1.0.0",
 		"s-age": "1.1.0",

From 7965e4609e0f4d241b7750b31fbec69fe446bcf3 Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Thu, 28 Dec 2017 20:40:53 +0000
Subject: [PATCH 166/186] fix(package): update riot to version 3.8.1

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index 68d5c2726..a2d5e3ce1 100644
--- a/package.json
+++ b/package.json
@@ -145,7 +145,7 @@
 		"redis": "2.8.0",
 		"request": "2.83.0",
 		"rimraf": "2.6.2",
-		"riot": "3.8.0",
+		"riot": "3.8.1",
 		"riot-tag-loader": "2.0.1",
 		"rndstr": "1.0.0",
 		"s-age": "1.1.0",

From 39c7e2ad1cdf9ba63810478414a2808d604c76d6 Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Thu, 28 Dec 2017 20:50:28 +0000
Subject: [PATCH 167/186] fix(package): update qrcode to version 1.2.0

Closes #1053
---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index 68d5c2726..605f24bf0 100644
--- a/package.json
+++ b/package.json
@@ -138,7 +138,7 @@
 		"prominence": "0.2.0",
 		"proxy-addr": "2.0.2",
 		"pug": "2.0.0-rc.4",
-		"qrcode": "1.0.1",
+		"qrcode": "1.2.0",
 		"ratelimiter": "3.0.3",
 		"recaptcha-promise": "0.1.3",
 		"reconnecting-websocket": "3.2.2",

From 9b4e3d95d15da5c7160dd60985912b6500955b95 Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Fri, 29 Dec 2017 09:50:12 +0000
Subject: [PATCH 168/186] fix(package): update riot-tag-loader to version 2.0.2

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index dcd880fbc..95759fa31 100644
--- a/package.json
+++ b/package.json
@@ -146,7 +146,7 @@
 		"request": "2.83.0",
 		"rimraf": "2.6.2",
 		"riot": "3.8.1",
-		"riot-tag-loader": "2.0.1",
+		"riot-tag-loader": "2.0.2",
 		"rndstr": "1.0.0",
 		"s-age": "1.1.0",
 		"seedrandom": "2.4.3",

From 4a6b4f7a2b589f53a02979d1a209612cb7629513 Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Sat, 30 Dec 2017 21:27:41 +0000
Subject: [PATCH 169/186] fix(package): update gulp-imagemin to version 4.1.0

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index dcd880fbc..80bfa5f4c 100644
--- a/package.json
+++ b/package.json
@@ -107,7 +107,7 @@
 		"gulp": "3.9.1",
 		"gulp-cssnano": "2.1.2",
 		"gulp-htmlmin": "4.0.0",
-		"gulp-imagemin": "4.0.0",
+		"gulp-imagemin": "4.1.0",
 		"gulp-mocha": "4.3.1",
 		"gulp-pug": "3.3.0",
 		"gulp-rename": "1.2.2",

From 59c64ba7d91be1fbedba3c7188150e0f81c76818 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 31 Dec 2017 14:38:41 +0900
Subject: [PATCH 170/186] Improve readability

---
 src/db/mongodb.ts | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/db/mongodb.ts b/src/db/mongodb.ts
index c978e6460..be234b365 100644
--- a/src/db/mongodb.ts
+++ b/src/db/mongodb.ts
@@ -1,8 +1,8 @@
 import config from '../conf';
 
 const uri = config.mongodb.user && config.mongodb.pass
-? `mongodb://${config.mongodb.user}:${config.mongodb.pass}@${config.mongodb.host}:${config.mongodb.port}/${config.mongodb.db}`
-: `mongodb://${config.mongodb.host}:${config.mongodb.port}/${config.mongodb.db}`;
+	? `mongodb://${config.mongodb.user}:${config.mongodb.pass}@${config.mongodb.host}:${config.mongodb.port}/${config.mongodb.db}`
+	: `mongodb://${config.mongodb.host}:${config.mongodb.port}/${config.mongodb.db}`;
 
 /**
  * monk

From deb86bf6e5fce7d97e449a1f7f2a7db48915a03d Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 31 Dec 2017 15:15:19 +0900
Subject: [PATCH 171/186] Fix bug

SEE:
https://github.com/mongodb/node-mongodb-native/blob/3.0.0/CHANGES_3.0.0.md
---
 src/db/mongodb.ts | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/db/mongodb.ts b/src/db/mongodb.ts
index be234b365..1263ccaac 100644
--- a/src/db/mongodb.ts
+++ b/src/db/mongodb.ts
@@ -24,9 +24,9 @@ const nativeDbConn = async (): Promise<mongodb.Db> => {
 	if (mdb) return mdb;
 
 	const db = await ((): Promise<mongodb.Db> => new Promise((resolve, reject) => {
-		mongodb.MongoClient.connect(uri, (e, db) => {
+		(mongodb as any).MongoClient.connect(uri, (e, client) => {
 			if (e) return reject(e);
-			resolve(db);
+			resolve(client.db(config.mongodb.db));
 		});
 	}))();
 

From 17369103d7d38e0d96083d331981e00c9565abb0 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=93=E3=81=B4=E3=81=AA=E3=81=9F=E3=81=BF=E3=81=BD?=
 <syuilotan@yahoo.co.jp>
Date: Mon, 1 Jan 2018 00:02:58 +0900
Subject: [PATCH 172/186] LICENSE: Update year to 2018

---
 LICENSE                   | 2 +-
 src/const.json            | 2 +-
 webpack/plugins/banner.ts | 2 +-
 3 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/LICENSE b/LICENSE
index e3733b396..0b6e30e45 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,6 +1,6 @@
 The MIT License (MIT)
 
-Copyright (c) 2014-2017 syuilo
+Copyright (c) 2014-2018 syuilo
 
 Permission is hereby granted, free of charge, to any person obtaining a copy
 of this software and associated documentation files (the "Software"), to deal
diff --git a/src/const.json b/src/const.json
index 0ee6ac206..d8fe4fe6c 100644
--- a/src/const.json
+++ b/src/const.json
@@ -1,5 +1,5 @@
 {
-	"copyright": "Copyright (c) 2014-2017 syuilo",
+	"copyright": "Copyright (c) 2014-2018 syuilo",
 	"themeColor": "#ff4e45",
 	"themeColorForeground": "#fff"
 }
diff --git a/webpack/plugins/banner.ts b/webpack/plugins/banner.ts
index 47b8cd355..a8774e0a3 100644
--- a/webpack/plugins/banner.ts
+++ b/webpack/plugins/banner.ts
@@ -3,7 +3,7 @@ import * as webpack from 'webpack';
 
 export default version => new webpack.BannerPlugin({
 	banner:
-		`Misskey v${version} | MIT Licensed, (c) syuilo 2014-2017\n` +
+		`Misskey v${version} | MIT Licensed, (c) syuilo 2014-2018\n` +
 		'https://github.com/syuilo/misskey\n' +
 		`built by ${os.hostname()} at ${new Date()}\n` +
 		'hash:[hash], chunkhash:[chunkhash]'

From 727ecf86b90cb128caa40ea4a3ab6aa36969543a Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 1 Jan 2018 01:58:12 +0900
Subject: [PATCH 173/186] Update dependencies :rocket:

---
 package.json | 30 +++++++++++++++---------------
 1 file changed, 15 insertions(+), 15 deletions(-)

diff --git a/package.json b/package.json
index 4df15d421..ff9069a42 100644
--- a/package.json
+++ b/package.json
@@ -23,9 +23,9 @@
 	},
 	"dependencies": {
 		"@fortawesome/fontawesome": "1.0.1",
-		"@fortawesome/fontawesome-free-brands": "5.0.1",
-		"@fortawesome/fontawesome-free-regular": "5.0.1",
-		"@fortawesome/fontawesome-free-solid": "5.0.1",
+		"@fortawesome/fontawesome-free-brands": "5.0.2",
+		"@fortawesome/fontawesome-free-regular": "5.0.2",
+		"@fortawesome/fontawesome-free-solid": "5.0.2",
 		"@prezzemolo/rap": "0.1.2",
 		"@prezzemolo/zip": "0.0.3",
 		"@types/bcryptjs": "2.4.1",
@@ -39,9 +39,9 @@
 		"@types/deep-equal": "1.0.1",
 		"@types/elasticsearch": "5.0.19",
 		"@types/eventemitter3": "2.0.2",
-		"@types/express": "4.0.39",
+		"@types/express": "4.11.0",
 		"@types/gm": "1.17.33",
-		"@types/gulp": "4.0.3",
+		"@types/gulp": "3.8.36",
 		"@types/gulp-htmlmin": "1.3.31",
 		"@types/gulp-mocha": "0.0.31",
 		"@types/gulp-rename": "0.0.33",
@@ -52,17 +52,17 @@
 		"@types/is-root": "1.0.0",
 		"@types/is-url": "1.2.28",
 		"@types/js-yaml": "3.10.1",
-		"@types/mkdirp": "^0.5.2",
-		"@types/mocha": "2.2.44",
-		"@types/mongodb": "2.2.17",
+		"@types/mkdirp": "0.5.2",
+		"@types/mocha": "2.2.45",
+		"@types/mongodb": "2.2.18",
 		"@types/monk": "1.0.6",
 		"@types/morgan": "1.7.35",
 		"@types/ms": "0.7.30",
 		"@types/multer": "1.3.6",
-		"@types/node": "8.5.1",
+		"@types/node": "8.5.2",
 		"@types/page": "1.5.32",
 		"@types/proxy-addr": "2.0.0",
-		"@types/pug": "^2.0.4",
+		"@types/pug": "2.0.4",
 		"@types/qrcode": "0.8.0",
 		"@types/ratelimiter": "2.1.28",
 		"@types/redis": "2.8.3",
@@ -108,11 +108,11 @@
 		"gulp-cssnano": "2.1.2",
 		"gulp-htmlmin": "4.0.0",
 		"gulp-imagemin": "4.1.0",
-		"gulp-mocha": "4.3.1",
+		"gulp-mocha": "5.0.0",
 		"gulp-pug": "3.3.0",
 		"gulp-rename": "1.2.2",
 		"gulp-replace": "0.6.1",
-		"gulp-stylus": "^2.6.0",
+		"gulp-stylus": "2.6.0",
 		"gulp-tslint": "8.1.2",
 		"gulp-typescript": "3.2.3",
 		"gulp-uglify": "3.0.0",
@@ -123,8 +123,8 @@
 		"is-url": "1.2.2",
 		"js-yaml": "3.10.0",
 		"mecab-async": "0.1.2",
-		"mkdirp": "^0.5.1",
-		"mocha": "4.0.1",
+		"mkdirp": "0.5.1",
+		"mocha": "4.1.0",
 		"moji": "0.5.1",
 		"mongodb": "3.0.1",
 		"monk": "6.0.5",
@@ -166,7 +166,7 @@
 		"ts-node": "4.1.0",
 		"tslint": "5.8.0",
 		"typescript": "2.6.2",
-		"uglify-es": "3.2.0",
+		"uglify-es": "3.3.4",
 		"uglifyjs-webpack-plugin": "1.1.5",
 		"uuid": "3.1.0",
 		"vhost": "3.0.2",

From 88e4f1b287e9fa6be12b5a2277073265d6732527 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 1 Jan 2018 02:08:41 +0900
Subject: [PATCH 174/186] :v:

---
 src/common/build/license.ts     | 13 +++++++++++++
 src/web/docs/license.en.pug     |  3 +++
 src/web/docs/license.ja.pug     |  3 +++
 src/web/docs/vars.ts            |  3 +++
 webpack/module/rules/license.ts |  9 ++-------
 5 files changed, 24 insertions(+), 7 deletions(-)
 create mode 100644 src/common/build/license.ts
 create mode 100644 src/web/docs/license.en.pug
 create mode 100644 src/web/docs/license.ja.pug

diff --git a/src/common/build/license.ts b/src/common/build/license.ts
new file mode 100644
index 000000000..e5c264df8
--- /dev/null
+++ b/src/common/build/license.ts
@@ -0,0 +1,13 @@
+import * as fs from 'fs';
+
+const license = fs.readFileSync(__dirname + '/../../../LICENSE', 'utf-8');
+
+const licenseHtml = license
+	.replace(/\r\n/g, '\n')
+	.replace(/(.)\n(.)/g, '$1 $2')
+	.replace(/(^|\n)(.*?)($|\n)/g, '<p>$2</p>');
+
+export {
+	license,
+	licenseHtml
+};
diff --git a/src/web/docs/license.en.pug b/src/web/docs/license.en.pug
new file mode 100644
index 000000000..240756e7e
--- /dev/null
+++ b/src/web/docs/license.en.pug
@@ -0,0 +1,3 @@
+h1 License
+
+div!= common.license
diff --git a/src/web/docs/license.ja.pug b/src/web/docs/license.ja.pug
new file mode 100644
index 000000000..1f44f3f5e
--- /dev/null
+++ b/src/web/docs/license.ja.pug
@@ -0,0 +1,3 @@
+h1 ライセンス
+
+div!= common.license
diff --git a/src/web/docs/vars.ts b/src/web/docs/vars.ts
index 65b224fbf..95ae9ee62 100644
--- a/src/web/docs/vars.ts
+++ b/src/web/docs/vars.ts
@@ -4,6 +4,7 @@ import * as yaml from 'js-yaml';
 
 import { fa } from '../../common/build/fa';
 import config from '../../conf';
+import { licenseHtml } from '../../common/build/license';
 const constants = require('../../const.json');
 
 export default function(): { [key: string]: any } {
@@ -42,5 +43,7 @@ export default function(): { [key: string]: any } {
 
 	vars['facss'] = fa.dom.css();
 
+	vars['license'] = licenseHtml;
+
 	return vars;
 }
diff --git a/webpack/module/rules/license.ts b/webpack/module/rules/license.ts
index 1795af960..de8b7d79f 100644
--- a/webpack/module/rules/license.ts
+++ b/webpack/module/rules/license.ts
@@ -2,13 +2,8 @@
  * Inject license
  */
 
-import * as fs from 'fs';
 const StringReplacePlugin = require('string-replace-webpack-plugin');
-
-const license = fs.readFileSync(__dirname + '/../../../LICENSE', 'utf-8')
-	.replace(/\r\n/g, '\n')
-	.replace(/(.)\n(.)/g, '$1 $2')
-	.replace(/(^|\n)(.*?)($|\n)/g, '<p>$2</p>');
+import { licenseHtml } from '../../../src/common/build/license';
 
 export default () => ({
 	enforce: 'pre',
@@ -16,7 +11,7 @@ export default () => ({
 	exclude: /node_modules/,
 	loader: StringReplacePlugin.replace({
 		replacements: [{
-			pattern: '%license%', replacement: () => license
+			pattern: '%license%', replacement: () => licenseHtml
 		}]
 	})
 });

From 77979ec37b732bb9c0797dc5d6c89330561791b4 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 1 Jan 2018 02:09:12 +0900
Subject: [PATCH 175/186] v3493

---
 CHANGELOG.md | 4 ++++
 package.json | 2 +-
 2 files changed, 5 insertions(+), 1 deletion(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index a05097ff0..6e69a7319 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,10 @@ ChangeLog (Release Notes)
 =========================
 主に notable な changes を書いていきます
 
+3493 (2018/01/01)
+-----------------
+* なんか
+
 3460 (2017/12/23)
 -----------------
 * 検索で複数のユーザーを指定できるように
diff --git a/package.json b/package.json
index ff9069a42..9245a6e2a 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
 	"name": "misskey",
 	"author": "syuilo <i@syuilo.com>",
-	"version": "0.0.3460",
+	"version": "0.0.3493",
 	"license": "MIT",
 	"description": "A miniblog-based SNS",
 	"bugs": "https://github.com/syuilo/misskey/issues",

From e93a2991a96d52f0a6b18f07d92798a4172fbe7f Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Mon, 1 Jan 2018 02:14:00 +0900
Subject: [PATCH 176/186] Update backup.md

---
 docs/backup.md | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/docs/backup.md b/docs/backup.md
index 484564b31..74ec2678e 100644
--- a/docs/backup.md
+++ b/docs/backup.md
@@ -7,7 +7,7 @@ Make sure **mongodb-tools** installed.
 
 In your shell:
 ``` shell
-$ mongodump --archive=db-backup
+$ mongodump --archive=db-backup -u <YourUserName> -p <YourPassword>
 ```
 
 For details, plese see [mongodump docs](https://docs.mongodb.com/manual/reference/program/mongodump/).

From f7cae37ce48a51b6d199635a05bb79132b8d5505 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 1 Jan 2018 02:26:25 +0900
Subject: [PATCH 177/186] Fix bug

---
 src/db/mongodb.ts | 7 +++++--
 1 file changed, 5 insertions(+), 2 deletions(-)

diff --git a/src/db/mongodb.ts b/src/db/mongodb.ts
index 1263ccaac..bbbe70c34 100644
--- a/src/db/mongodb.ts
+++ b/src/db/mongodb.ts
@@ -1,7 +1,10 @@
 import config from '../conf';
 
-const uri = config.mongodb.user && config.mongodb.pass
-	? `mongodb://${config.mongodb.user}:${config.mongodb.pass}@${config.mongodb.host}:${config.mongodb.port}/${config.mongodb.db}`
+const u = config.mongodb.user ? encodeURIComponent(config.mongodb.user) : null;
+const p = config.mongodb.pass ? encodeURIComponent(config.mongodb.pass) : null;
+
+const uri = u && p
+	? `mongodb://${u}:${p}@${config.mongodb.host}:${config.mongodb.port}/${config.mongodb.db}`
 	: `mongodb://${config.mongodb.host}:${config.mongodb.port}/${config.mongodb.db}`;
 
 /**

From 6b9f6c6e3b2f82ea2a466614626b7bfaa6ad9286 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 8 Jan 2018 01:47:56 +0900
Subject: [PATCH 178/186] Show the licenses in the doc

---
 package.json                 |  2 ++
 src/web/docs/api/gulpfile.ts |  8 ++++----
 src/web/docs/gulpfile.ts     |  4 ++--
 src/web/docs/license.en.pug  | 14 ++++++++++++++
 src/web/docs/license.ja.pug  | 14 ++++++++++++++
 src/web/docs/style.styl      |  2 ++
 src/web/docs/vars.ts         | 17 ++++++++++++++++-
 7 files changed, 54 insertions(+), 7 deletions(-)

diff --git a/package.json b/package.json
index 9245a6e2a..69c92efdf 100644
--- a/package.json
+++ b/package.json
@@ -52,6 +52,7 @@
 		"@types/is-root": "1.0.0",
 		"@types/is-url": "1.2.28",
 		"@types/js-yaml": "3.10.1",
+		"@types/license-checker": "^15.0.0",
 		"@types/mkdirp": "0.5.2",
 		"@types/mocha": "2.2.45",
 		"@types/mongodb": "2.2.18",
@@ -122,6 +123,7 @@
 		"is-root": "1.0.0",
 		"is-url": "1.2.2",
 		"js-yaml": "3.10.0",
+		"license-checker": "^15.0.0",
 		"mecab-async": "0.1.2",
 		"mkdirp": "0.5.1",
 		"mocha": "4.1.0",
diff --git a/src/web/docs/api/gulpfile.ts b/src/web/docs/api/gulpfile.ts
index 4c30871a0..cd1bf1530 100644
--- a/src/web/docs/api/gulpfile.ts
+++ b/src/web/docs/api/gulpfile.ts
@@ -17,8 +17,6 @@ import config from './../../../conf';
 
 import generateVars from '../vars';
 
-const commonVars = generateVars();
-
 const langs = Object.keys(locales);
 
 const kebab = string => string.replace(/([a-z])([A-Z])/g, '$1-$2').replace(/\s+/g, '-').toLowerCase();
@@ -94,7 +92,8 @@ gulp.task('doc:api', [
 	'doc:api:entities'
 ]);
 
-gulp.task('doc:api:endpoints', () => {
+gulp.task('doc:api:endpoints', async () => {
+	const commonVars = await generateVars();
 	glob('./src/web/docs/api/endpoints/**/*.yaml', (globErr, files) => {
 		if (globErr) {
 			console.error(globErr);
@@ -144,7 +143,8 @@ gulp.task('doc:api:endpoints', () => {
 	});
 });
 
-gulp.task('doc:api:entities', () => {
+gulp.task('doc:api:entities', async () => {
+	const commonVars = await generateVars();
 	glob('./src/web/docs/api/entities/**/*.yaml', (globErr, files) => {
 		if (globErr) {
 			console.error(globErr);
diff --git a/src/web/docs/gulpfile.ts b/src/web/docs/gulpfile.ts
index 71033e1bc..d5ddda108 100644
--- a/src/web/docs/gulpfile.ts
+++ b/src/web/docs/gulpfile.ts
@@ -23,9 +23,9 @@ gulp.task('doc', [
 	'doc:styles'
 ]);
 
-const commonVars = generateVars();
+gulp.task('doc:docs', async () => {
+	const commonVars = await generateVars();
 
-gulp.task('doc:docs', () => {
 	glob('./src/web/docs/**/*.*.pug', (globErr, files) => {
 		if (globErr) {
 			console.error(globErr);
diff --git a/src/web/docs/license.en.pug b/src/web/docs/license.en.pug
index 240756e7e..45d8b7647 100644
--- a/src/web/docs/license.en.pug
+++ b/src/web/docs/license.en.pug
@@ -1,3 +1,17 @@
 h1 License
 
 div!= common.license
+
+details
+	summary Libraries
+
+	section
+		h2 Libraries
+
+		each dependency, name in common.dependencies
+			details
+				summary= name
+
+				section
+					h3= name
+					pre= dependency.licenseText
diff --git a/src/web/docs/license.ja.pug b/src/web/docs/license.ja.pug
index 1f44f3f5e..7bd9a6294 100644
--- a/src/web/docs/license.ja.pug
+++ b/src/web/docs/license.ja.pug
@@ -1,3 +1,17 @@
 h1 ライセンス
 
 div!= common.license
+
+details
+	summary ライブラリ
+
+	section
+		h2 ライブラリ
+
+		each dependency, name in common.dependencies
+			details
+				summary= name
+
+				section
+					h3= name
+					pre= dependency.licenseText
diff --git a/src/web/docs/style.styl b/src/web/docs/style.styl
index a726d49b1..bc165f872 100644
--- a/src/web/docs/style.styl
+++ b/src/web/docs/style.styl
@@ -114,5 +114,7 @@ code
 	border-radius 4px
 
 pre
+	overflow auto
+
 	> code
 		display block
diff --git a/src/web/docs/vars.ts b/src/web/docs/vars.ts
index 95ae9ee62..6f713f21d 100644
--- a/src/web/docs/vars.ts
+++ b/src/web/docs/vars.ts
@@ -1,13 +1,16 @@
 import * as fs from 'fs';
+import * as util from 'util';
 import * as glob from 'glob';
 import * as yaml from 'js-yaml';
+import * as licenseChecker from 'license-checker';
+import * as tmp from 'tmp';
 
 import { fa } from '../../common/build/fa';
 import config from '../../conf';
 import { licenseHtml } from '../../common/build/license';
 const constants = require('../../const.json');
 
-export default function(): { [key: string]: any } {
+export default async function(): Promise<{ [key: string]: any }> {
 	const vars = {} as { [key: string]: any };
 
 	const endpoints = glob.sync('./src/web/docs/api/endpoints/**/*.yaml');
@@ -45,5 +48,17 @@ export default function(): { [key: string]: any } {
 
 	vars['license'] = licenseHtml;
 
+	const tmpObj = tmp.fileSync();
+	fs.writeFileSync(tmpObj.name, JSON.stringify({
+		licenseText: ''
+	}), 'utf-8');
+	const dependencies = await util.promisify(licenseChecker.init).bind(licenseChecker)({
+		start: __dirname + '/../../../',
+		customPath: tmpObj.name
+	});
+	tmpObj.removeCallback();
+
+	vars['dependencies'] = dependencies;
+
 	return vars;
 }

From d8f8730a7423c2c7a3ccc0365680319c2ff57f14 Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Fri, 19 Jan 2018 09:22:30 +0900
Subject: [PATCH 179/186] Update api.ts

---
 src/web/app/common/scripts/api.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/web/app/common/scripts/api.ts b/src/web/app/common/scripts/api.ts
index 2008e6f5a..bba838f56 100644
--- a/src/web/app/common/scripts/api.ts
+++ b/src/web/app/common/scripts/api.ts
@@ -40,7 +40,7 @@ export default (i, endpoint, data = {}): Promise<{ [x: string]: any }> => {
 			} else {
 				res.json().then(err => {
 					reject(err.error);
-				});
+				}, reject);
 			}
 		}).catch(reject);
 	});

From 50528955189a388ef2ad6f984e786d24c895c09a Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Fri, 19 Jan 2018 20:34:01 +0900
Subject: [PATCH 180/186] Refactor

---
 src/api/common/add-file-to-drive.ts | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/api/common/add-file-to-drive.ts b/src/api/common/add-file-to-drive.ts
index 427b54d72..23cbc44e6 100644
--- a/src/api/common/add-file-to-drive.ts
+++ b/src/api/common/add-file-to-drive.ts
@@ -266,11 +266,11 @@ export default (user: any, file: string | stream.Readable, ...args) => new Promi
 		}
 		rej(new Error('un-compatible file.'));
 	})
-	.then(([path, remove]): Promise<any> => new Promise((res, rej) => {
+	.then(([path, shouldCleanup]): Promise<any> => new Promise((res, rej) => {
 		addFile(user, path, ...args)
 			.then(file => {
 				res(file);
-				if (remove) {
+				if (shouldCleanup) {
 					fs.unlink(path, (e) => {
 						if (e) log(e.stack);
 					});

From 78e99315241a384dc246a78c399e9a8955ab8eb9 Mon Sep 17 00:00:00 2001
From: Aya Morisawa <AyaMorisawa4869@gmail.com>
Date: Sun, 21 Jan 2018 15:49:31 +0900
Subject: [PATCH 181/186] Update text.js

---
 test/text.js | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/test/text.js b/test/text.js
index 49e2f02b5..24800ac44 100644
--- a/test/text.js
+++ b/test/text.js
@@ -8,7 +8,7 @@ const analyze = require('../built/api/common/text').default;
 const syntaxhighlighter = require('../built/api/common/text/core/syntax-highlighter').default;
 
 describe('Text', () => {
-	it('is correctly analyzed', () => {
+	it('can be analyzed', () => {
 		const tokens = analyze('@himawari お腹ペコい :cat: #yryr');
 		assert.deepEqual([
 			{ type: 'mention', content: '@himawari', username: 'himawari' },
@@ -19,7 +19,7 @@ describe('Text', () => {
 		], tokens);
 	});
 
-	it('逆関数で正しく復元できる', () => {
+	it('can be inverted', () => {
 		const text = '@himawari お腹ペコい :cat: #yryr';
 		assert.equal(analyze(text).map(x => x.content).join(''), text);
 	});

From 1dab37bdaebf54d36085068787890c3983b86191 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 2 Feb 2018 07:34:51 +0900
Subject: [PATCH 182/186] Update dependencies :rocket:

---
 package.json      | 60 +++++++++++++++++++++++------------------------
 src/db/mongodb.ts |  2 +-
 2 files changed, 31 insertions(+), 31 deletions(-)

diff --git a/package.json b/package.json
index 69c92efdf..bd5114480 100644
--- a/package.json
+++ b/package.json
@@ -30,7 +30,7 @@
 		"@prezzemolo/zip": "0.0.3",
 		"@types/bcryptjs": "2.4.1",
 		"@types/body-parser": "1.16.8",
-		"@types/chai": "4.0.10",
+		"@types/chai": "4.1.2",
 		"@types/chai-http": "3.0.3",
 		"@types/compression": "0.0.35",
 		"@types/cookie": "0.3.1",
@@ -39,7 +39,7 @@
 		"@types/deep-equal": "1.0.1",
 		"@types/elasticsearch": "5.0.19",
 		"@types/eventemitter3": "2.0.2",
-		"@types/express": "4.11.0",
+		"@types/express": "4.11.1",
 		"@types/gm": "1.17.33",
 		"@types/gulp": "3.8.36",
 		"@types/gulp-htmlmin": "1.3.31",
@@ -54,55 +54,55 @@
 		"@types/js-yaml": "3.10.1",
 		"@types/license-checker": "^15.0.0",
 		"@types/mkdirp": "0.5.2",
-		"@types/mocha": "2.2.45",
-		"@types/mongodb": "2.2.18",
-		"@types/monk": "1.0.6",
+		"@types/mocha": "2.2.47",
+		"@types/mongodb": "3.0.5",
+		"@types/monk": "6.0.0",
 		"@types/morgan": "1.7.35",
 		"@types/ms": "0.7.30",
 		"@types/multer": "1.3.6",
-		"@types/node": "8.5.2",
+		"@types/node": "9.4.0",
 		"@types/page": "1.5.32",
 		"@types/proxy-addr": "2.0.0",
 		"@types/pug": "2.0.4",
 		"@types/qrcode": "0.8.0",
 		"@types/ratelimiter": "2.1.28",
-		"@types/redis": "2.8.3",
-		"@types/request": "2.0.9",
+		"@types/redis": "2.8.5",
+		"@types/request": "2.47.0",
 		"@types/rimraf": "2.0.2",
 		"@types/riot": "3.6.1",
 		"@types/seedrandom": "2.4.27",
 		"@types/serve-favicon": "2.2.30",
-		"@types/speakeasy": "2.0.1",
+		"@types/speakeasy": "2.0.2",
 		"@types/tmp": "0.0.33",
 		"@types/uuid": "3.4.3",
-		"@types/webpack": "3.8.1",
+		"@types/webpack": "3.8.4",
 		"@types/webpack-stream": "3.2.8",
-		"@types/websocket": "0.0.35",
+		"@types/websocket": "0.0.36",
 		"accesses": "2.5.0",
 		"animejs": "2.2.0",
 		"autwh": "0.0.1",
 		"awesome-typescript-loader": "3.4.1",
 		"bcryptjs": "2.4.3",
 		"body-parser": "1.18.2",
-		"cafy": "3.2.0",
+		"cafy": "3.2.1",
 		"chai": "4.1.2",
 		"chai-http": "3.0.0",
 		"chalk": "2.3.0",
 		"compression": "1.7.1",
 		"cookie": "0.3.1",
 		"cors": "2.8.4",
-		"cropperjs": "1.2.1",
-		"css-loader": "0.28.7",
+		"cropperjs": "1.2.2",
+		"css-loader": "0.28.9",
 		"debug": "3.1.0",
 		"deep-equal": "1.0.1",
 		"deepcopy": "0.6.3",
 		"diskusage": "0.2.4",
-		"elasticsearch": "14.0.0",
+		"elasticsearch": "14.1.0",
 		"escape-regexp": "0.0.1",
 		"eventemitter3": "3.0.0",
 		"exif-js": "2.3.0",
 		"express": "4.16.2",
-		"file-type": "7.4.0",
+		"file-type": "7.5.0",
 		"fuckadblock": "3.2.1",
 		"gm": "1.23.1",
 		"gulp": "3.9.1",
@@ -113,29 +113,29 @@
 		"gulp-pug": "3.3.0",
 		"gulp-rename": "1.2.2",
 		"gulp-replace": "0.6.1",
-		"gulp-stylus": "2.6.0",
+		"gulp-stylus": "2.7.0",
 		"gulp-tslint": "8.1.2",
-		"gulp-typescript": "3.2.3",
+		"gulp-typescript": "3.2.4",
 		"gulp-uglify": "3.0.0",
 		"gulp-util": "3.0.8",
 		"highlight.js": "9.12.0",
-		"inquirer": "4.0.1",
+		"inquirer": "5.0.1",
 		"is-root": "1.0.0",
 		"is-url": "1.2.2",
 		"js-yaml": "3.10.0",
-		"license-checker": "^15.0.0",
+		"license-checker": "16.0.0",
 		"mecab-async": "0.1.2",
 		"mkdirp": "0.5.1",
-		"mocha": "4.1.0",
+		"mocha": "5.0.0",
 		"moji": "0.5.1",
-		"mongodb": "3.0.1",
+		"mongodb": "3.0.2",
 		"monk": "6.0.5",
 		"morgan": "1.9.0",
 		"ms": "2.1.1",
 		"multer": "1.3.0",
 		"nprogress": "0.2.0",
 		"os-utils": "0.0.14",
-		"page": "1.7.1",
+		"page": "1.8.3",
 		"pictograph": "2.1.5",
 		"prominence": "0.2.0",
 		"proxy-addr": "2.0.2",
@@ -150,13 +150,13 @@
 		"riot": "3.8.1",
 		"riot-tag-loader": "2.0.2",
 		"rndstr": "1.0.0",
-		"s-age": "1.1.0",
+		"s-age": "1.1.2",
 		"seedrandom": "2.4.3",
 		"serve-favicon": "2.4.5",
 		"sortablejs": "1.7.0",
 		"speakeasy": "2.0.0",
 		"string-replace-webpack-plugin": "0.1.3",
-		"style-loader": "0.19.1",
+		"style-loader": "0.20.1",
 		"stylus": "0.54.5",
 		"stylus-loader": "3.0.1",
 		"summaly": "2.0.3",
@@ -166,11 +166,11 @@
 		"textarea-caret": "3.0.2",
 		"tmp": "0.0.33",
 		"ts-node": "4.1.0",
-		"tslint": "5.8.0",
-		"typescript": "2.6.2",
-		"uglify-es": "3.3.4",
-		"uglifyjs-webpack-plugin": "1.1.5",
-		"uuid": "3.1.0",
+		"tslint": "5.9.1",
+		"typescript": "2.7.1",
+		"uglify-es": "3.3.9",
+		"uglifyjs-webpack-plugin": "1.1.8",
+		"uuid": "3.2.1",
 		"vhost": "3.0.2",
 		"web-push": "3.2.5",
 		"webpack": "3.10.0",
diff --git a/src/db/mongodb.ts b/src/db/mongodb.ts
index bbbe70c34..233f2f3d7 100644
--- a/src/db/mongodb.ts
+++ b/src/db/mongodb.ts
@@ -10,7 +10,7 @@ const uri = u && p
 /**
  * monk
  */
-import * as mongo from 'monk';
+import mongo from 'monk';
 
 const db = mongo(uri);
 

From 9a282e37be5ba847718d198d30eb97f31d11f2a0 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 2 Feb 2018 08:06:01 +0900
Subject: [PATCH 183/186] wip

---
 src/api/models/app.ts                    |  95 ++++++++++-
 src/api/models/auth-session.ts           |  44 ++++-
 src/api/models/channel.ts                |  66 +++++++-
 src/api/models/drive-file.ts             |  84 +++++++++-
 src/api/models/drive-folder.ts           |  67 +++++++-
 src/api/models/messaging-message.ts      |  66 +++++++-
 src/api/models/notification.ts           |  64 +++++++-
 src/api/models/post-reaction.ts          |  47 +++++-
 src/api/models/post.ts                   | 189 +++++++++++++++++++++-
 src/api/models/signin.ts                 |  28 +++-
 src/api/models/user.ts                   | 196 ++++++++++++++++++++++-
 src/api/serializers/app.ts               |  83 ----------
 src/api/serializers/auth-session.ts      |  40 -----
 src/api/serializers/channel.ts           |  66 --------
 src/api/serializers/drive-file.ts        |  78 ---------
 src/api/serializers/drive-folder.ts      |  64 --------
 src/api/serializers/drive-tag.ts         |  35 ----
 src/api/serializers/messaging-message.ts |  68 --------
 src/api/serializers/notification.ts      |  65 --------
 src/api/serializers/post-reaction.ts     |  43 -----
 src/api/serializers/post.ts              | 192 ----------------------
 src/api/serializers/signin.ts            |  23 ---
 src/api/serializers/user.ts              | 190 ----------------------
 23 files changed, 920 insertions(+), 973 deletions(-)
 delete mode 100644 src/api/serializers/app.ts
 delete mode 100644 src/api/serializers/auth-session.ts
 delete mode 100644 src/api/serializers/channel.ts
 delete mode 100644 src/api/serializers/drive-file.ts
 delete mode 100644 src/api/serializers/drive-folder.ts
 delete mode 100644 src/api/serializers/drive-tag.ts
 delete mode 100644 src/api/serializers/messaging-message.ts
 delete mode 100644 src/api/serializers/notification.ts
 delete mode 100644 src/api/serializers/post-reaction.ts
 delete mode 100644 src/api/serializers/post.ts
 delete mode 100644 src/api/serializers/signin.ts
 delete mode 100644 src/api/serializers/user.ts

diff --git a/src/api/models/app.ts b/src/api/models/app.ts
index 68f2f448b..fe9d49ff6 100644
--- a/src/api/models/app.ts
+++ b/src/api/models/app.ts
@@ -1,13 +1,96 @@
+import * as mongo from 'mongodb';
+import deepcopy = require('deepcopy');
+import AccessToken from './access-token';
 import db from '../../db/mongodb';
+import config from '../../conf';
 
-const collection = db.get('apps');
+const App = db.get<IApp>('apps');
+App.createIndex('name_id');
+App.createIndex('name_id_lower');
+App.createIndex('secret');
+export default App;
 
-(collection as any).createIndex('name_id'); // fuck type definition
-(collection as any).createIndex('name_id_lower'); // fuck type definition
-(collection as any).createIndex('secret'); // fuck type definition
-
-export default collection as any; // fuck type definition
+export type IApp = {
+	_id: mongo.ObjectID;
+	created_at: Date;
+	user_id: mongo.ObjectID;
+};
 
 export function isValidNameId(nameId: string): boolean {
 	return typeof nameId == 'string' && /^[a-zA-Z0-9\-]{3,30}$/.test(nameId);
 }
+
+/**
+ * Pack an app for API response
+ *
+ * @param {any} app
+ * @param {any} me?
+ * @param {any} options?
+ * @return {Promise<any>}
+ */
+export const pack = (
+	app: any,
+	me?: any,
+	options?: {
+		includeSecret?: boolean,
+		includeProfileImageIds?: boolean
+	}
+) => new Promise<any>(async (resolve, reject) => {
+	const opts = options || {
+		includeSecret: false,
+		includeProfileImageIds: false
+	};
+
+	let _app: any;
+
+	// Populate the app if 'app' is ID
+	if (mongo.ObjectID.prototype.isPrototypeOf(app)) {
+		_app = await App.findOne({
+			_id: app
+		});
+	} else if (typeof app === 'string') {
+		_app = await App.findOne({
+			_id: new mongo.ObjectID(app)
+		});
+	} else {
+		_app = deepcopy(app);
+	}
+
+	// Me
+	if (me && !mongo.ObjectID.prototype.isPrototypeOf(me)) {
+		if (typeof me === 'string') {
+			me = new mongo.ObjectID(me);
+		} else {
+			me = me._id;
+		}
+	}
+
+	// Rename _id to id
+	_app.id = _app._id;
+	delete _app._id;
+
+	delete _app.name_id_lower;
+
+	// Visible by only owner
+	if (!opts.includeSecret) {
+		delete _app.secret;
+	}
+
+	_app.icon_url = _app.icon != null
+		? `${config.drive_url}/${_app.icon}`
+		: `${config.drive_url}/app-default.jpg`;
+
+	if (me) {
+		// 既に連携しているか
+		const exist = await AccessToken.count({
+			app_id: _app.id,
+			user_id: me,
+		}, {
+				limit: 1
+			});
+
+		_app.is_authorized = exist === 1;
+	}
+
+	resolve(_app);
+});
diff --git a/src/api/models/auth-session.ts b/src/api/models/auth-session.ts
index b264a133e..997ec61c2 100644
--- a/src/api/models/auth-session.ts
+++ b/src/api/models/auth-session.ts
@@ -1,3 +1,45 @@
+import * as mongo from 'mongodb';
+import deepcopy = require('deepcopy');
 import db from '../../db/mongodb';
+import { pack as packApp } from './app';
 
-export default db.get('auth_sessions') as any; // fuck type definition
+const AuthSession = db.get('auth_sessions');
+export default AuthSession;
+
+export interface IAuthSession {
+	_id: mongo.ObjectID;
+}
+
+/**
+ * Pack an auth session for API response
+ *
+ * @param {any} session
+ * @param {any} me?
+ * @return {Promise<any>}
+ */
+export const pack = (
+	session: any,
+	me?: any
+) => new Promise<any>(async (resolve, reject) => {
+	let _session: any;
+
+	// TODO: Populate session if it ID
+
+	_session = deepcopy(session);
+
+	// Me
+	if (me && !mongo.ObjectID.prototype.isPrototypeOf(me)) {
+		if (typeof me === 'string') {
+			me = new mongo.ObjectID(me);
+		} else {
+			me = me._id;
+		}
+	}
+
+	delete _session._id;
+
+	// Populate app
+	_session.app = await packApp(_session.app_id, me);
+
+	resolve(_session);
+});
diff --git a/src/api/models/channel.ts b/src/api/models/channel.ts
index c80e84dbc..815d53593 100644
--- a/src/api/models/channel.ts
+++ b/src/api/models/channel.ts
@@ -1,9 +1,11 @@
 import * as mongo from 'mongodb';
+import deepcopy = require('deepcopy');
+import { IUser } from './user';
+import Watching from './channel-watching';
 import db from '../../db/mongodb';
 
-const collection = db.get('channels');
-
-export default collection as any; // fuck type definition
+const Channel = db.get<IChannel>('channels');
+export default Channel;
 
 export type IChannel = {
 	_id: mongo.ObjectID;
@@ -12,3 +14,61 @@ export type IChannel = {
 	user_id: mongo.ObjectID;
 	index: number;
 };
+
+/**
+ * Pack a channel for API response
+ *
+ * @param channel target
+ * @param me? serializee
+ * @return response
+ */
+export const pack = (
+	channel: string | mongo.ObjectID | IChannel,
+	me?: string | mongo.ObjectID | IUser
+) => new Promise<any>(async (resolve, reject) => {
+
+	let _channel: any;
+
+	// Populate the channel if 'channel' is ID
+	if (mongo.ObjectID.prototype.isPrototypeOf(channel)) {
+		_channel = await Channel.findOne({
+			_id: channel
+		});
+	} else if (typeof channel === 'string') {
+		_channel = await Channel.findOne({
+			_id: new mongo.ObjectID(channel)
+		});
+	} else {
+		_channel = deepcopy(channel);
+	}
+
+	// Rename _id to id
+	_channel.id = _channel._id;
+	delete _channel._id;
+
+	// Remove needless properties
+	delete _channel.user_id;
+
+	// Me
+	const meId: mongo.ObjectID = me
+	? mongo.ObjectID.prototype.isPrototypeOf(me)
+		? me as mongo.ObjectID
+		: typeof me === 'string'
+			? new mongo.ObjectID(me)
+			: (me as IUser)._id
+	: null;
+
+	if (me) {
+		//#region Watchしているかどうか
+		const watch = await Watching.findOne({
+			user_id: meId,
+			channel_id: _channel.id,
+			deleted_at: { $exists: false }
+		});
+
+		_channel.is_watching = watch !== null;
+		//#endregion
+	}
+
+	resolve(_channel);
+});
diff --git a/src/api/models/drive-file.ts b/src/api/models/drive-file.ts
index 802ee5a5f..6a8db3ad4 100644
--- a/src/api/models/drive-file.ts
+++ b/src/api/models/drive-file.ts
@@ -1,9 +1,12 @@
 import * as mongodb from 'mongodb';
+import deepcopy = require('deepcopy');
+import { pack as packFolder } from './drive-folder';
+import config from '../../conf';
 import monkDb, { nativeDbConn } from '../../db/mongodb';
 
-const collection = monkDb.get('drive_files.files');
+const DriveFile = monkDb.get<IDriveFile>('drive_files.files');
 
-export default collection as any; // fuck type definition
+export default DriveFile;
 
 const getGridFSBucket = async (): Promise<mongodb.GridFSBucket> => {
 	const db = await nativeDbConn();
@@ -15,6 +18,12 @@ const getGridFSBucket = async (): Promise<mongodb.GridFSBucket> => {
 
 export { getGridFSBucket };
 
+export type IDriveFile = {
+	_id: mongodb.ObjectID;
+	created_at: Date;
+	user_id: mongodb.ObjectID;
+};
+
 export function validateFileName(name: string): boolean {
 	return (
 		(name.trim().length > 0) &&
@@ -24,3 +33,74 @@ export function validateFileName(name: string): boolean {
 		(name.indexOf('..') === -1)
 	);
 }
+
+/**
+ * Pack a drive file for API response
+ *
+ * @param {any} file
+ * @param {any} options?
+ * @return {Promise<any>}
+ */
+export const pack = (
+	file: any,
+	options?: {
+		detail: boolean
+	}
+) => new Promise<any>(async (resolve, reject) => {
+	const opts = Object.assign({
+		detail: false
+	}, options);
+
+	let _file: any;
+
+	// Populate the file if 'file' is ID
+	if (mongodb.ObjectID.prototype.isPrototypeOf(file)) {
+		_file = await DriveFile.findOne({
+			_id: file
+		});
+	} else if (typeof file === 'string') {
+		_file = await DriveFile.findOne({
+			_id: new mongodb.ObjectID(file)
+		});
+	} else {
+		_file = deepcopy(file);
+	}
+
+	if (!_file) return reject('invalid file arg.');
+
+	// rendered target
+	let _target: any = {};
+
+	_target.id = _file._id;
+	_target.created_at = _file.uploadDate;
+	_target.name = _file.filename;
+	_target.type = _file.contentType;
+	_target.datasize = _file.length;
+	_target.md5 = _file.md5;
+
+	_target = Object.assign(_target, _file.metadata);
+
+	_target.url = `${config.drive_url}/${_target.id}/${encodeURIComponent(_target.name)}`;
+
+	if (_target.properties == null) _target.properties = {};
+
+	if (opts.detail) {
+		if (_target.folder_id) {
+			// Populate folder
+			_target.folder = await packFolder(_target.folder_id, {
+				detail: true
+			});
+		}
+
+		/*
+		if (_target.tags) {
+			// Populate tags
+			_target.tags = await _target.tags.map(async (tag: any) =>
+				await serializeDriveTag(tag)
+			);
+		}
+		*/
+	}
+
+	resolve(_target);
+});
diff --git a/src/api/models/drive-folder.ts b/src/api/models/drive-folder.ts
index f81ffe855..48b26c2bd 100644
--- a/src/api/models/drive-folder.ts
+++ b/src/api/models/drive-folder.ts
@@ -1,6 +1,16 @@
+import * as mongo from 'mongodb';
+import deepcopy = require('deepcopy');
 import db from '../../db/mongodb';
+import DriveFile from './drive-file';
 
-export default db.get('drive_folders') as any; // fuck type definition
+const DriveFolder = db.get<IDriveFolder>('drive_folders');
+export default DriveFolder;
+
+export type IDriveFolder = {
+	_id: mongo.ObjectID;
+	created_at: Date;
+	user_id: mongo.ObjectID;
+};
 
 export function isValidFolderName(name: string): boolean {
 	return (
@@ -8,3 +18,58 @@ export function isValidFolderName(name: string): boolean {
 		(name.length <= 200)
 	);
 }
+
+/**
+ * Pack a drive folder for API response
+ *
+ * @param {any} folder
+ * @param {any} options?
+ * @return {Promise<any>}
+ */
+export const pack = (
+	folder: any,
+	options?: {
+		detail: boolean
+	}
+) => new Promise<any>(async (resolve, reject) => {
+	const opts = Object.assign({
+		detail: false
+	}, options);
+
+	let _folder: any;
+
+	// Populate the folder if 'folder' is ID
+	if (mongo.ObjectID.prototype.isPrototypeOf(folder)) {
+		_folder = await DriveFolder.findOne({ _id: folder });
+	} else if (typeof folder === 'string') {
+		_folder = await DriveFolder.findOne({ _id: new mongo.ObjectID(folder) });
+	} else {
+		_folder = deepcopy(folder);
+	}
+
+	// Rename _id to id
+	_folder.id = _folder._id;
+	delete _folder._id;
+
+	if (opts.detail) {
+		const childFoldersCount = await DriveFolder.count({
+			parent_id: _folder.id
+		});
+
+		const childFilesCount = await DriveFile.count({
+			'metadata.folder_id': _folder.id
+		});
+
+		_folder.folders_count = childFoldersCount;
+		_folder.files_count = childFilesCount;
+	}
+
+	if (opts.detail && _folder.parent_id) {
+		// Populate parent folder
+		_folder.parent = await pack(_folder.parent_id, {
+			detail: true
+		});
+	}
+
+	resolve(_folder);
+});
diff --git a/src/api/models/messaging-message.ts b/src/api/models/messaging-message.ts
index 18afa57e4..ffdda1db2 100644
--- a/src/api/models/messaging-message.ts
+++ b/src/api/models/messaging-message.ts
@@ -1,7 +1,12 @@
 import * as mongo from 'mongodb';
+import deepcopy = require('deepcopy');
+import { pack as packUser } from './user';
+import { pack as packFile } from './drive-file';
 import db from '../../db/mongodb';
+import parse from '../common/text';
 
-export default db.get('messaging_messages') as any; // fuck type definition
+const MessagingMessage = db.get<IMessagingMessage>('messaging_messages');
+export default MessagingMessage;
 
 export interface IMessagingMessage {
 	_id: mongo.ObjectID;
@@ -10,3 +15,62 @@ export interface IMessagingMessage {
 export function isValidText(text: string): boolean {
 	return text.length <= 1000 && text.trim() != '';
 }
+
+/**
+ * Pack a messaging message for API response
+ *
+ * @param {any} message
+ * @param {any} me?
+ * @param {any} options?
+ * @return {Promise<any>}
+ */
+export const pack = (
+	message: any,
+	me?: any,
+	options?: {
+		populateRecipient: boolean
+	}
+) => new Promise<any>(async (resolve, reject) => {
+	const opts = options || {
+		populateRecipient: true
+	};
+
+	let _message: any;
+
+	// Populate the message if 'message' is ID
+	if (mongo.ObjectID.prototype.isPrototypeOf(message)) {
+		_message = await MessagingMessage.findOne({
+			_id: message
+		});
+	} else if (typeof message === 'string') {
+		_message = await MessagingMessage.findOne({
+			_id: new mongo.ObjectID(message)
+		});
+	} else {
+		_message = deepcopy(message);
+	}
+
+	// Rename _id to id
+	_message.id = _message._id;
+	delete _message._id;
+
+	// Parse text
+	if (_message.text) {
+		_message.ast = parse(_message.text);
+	}
+
+	// Populate user
+	_message.user = await packUser(_message.user_id, me);
+
+	if (_message.file) {
+		// Populate file
+		_message.file = await packFile(_message.file_id);
+	}
+
+	if (opts.populateRecipient) {
+		// Populate recipient
+		_message.recipient = await packUser(_message.recipient_id, me);
+	}
+
+	resolve(_message);
+});
diff --git a/src/api/models/notification.ts b/src/api/models/notification.ts
index e3dc6c70a..fa7049d31 100644
--- a/src/api/models/notification.ts
+++ b/src/api/models/notification.ts
@@ -1,8 +1,11 @@
 import * as mongo from 'mongodb';
+import deepcopy = require('deepcopy');
 import db from '../../db/mongodb';
-import { IUser } from './user';
+import { IUser, pack as packUser } from './user';
+import { pack as packPost } from './post';
 
-export default db.get('notifications') as any; // fuck type definition
+const Notification = db.get<INotification>('notifications');
+export default Notification;
 
 export interface INotification {
 	_id: mongo.ObjectID;
@@ -45,3 +48,60 @@ export interface INotification {
 	 */
 	is_read: Boolean;
 }
+
+/**
+ * Pack a notification for API response
+ *
+ * @param {any} notification
+ * @return {Promise<any>}
+ */
+export const pack = (notification: any) => new Promise<any>(async (resolve, reject) => {
+	let _notification: any;
+
+	// Populate the notification if 'notification' is ID
+	if (mongo.ObjectID.prototype.isPrototypeOf(notification)) {
+		_notification = await Notification.findOne({
+			_id: notification
+		});
+	} else if (typeof notification === 'string') {
+		_notification = await Notification.findOne({
+			_id: new mongo.ObjectID(notification)
+		});
+	} else {
+		_notification = deepcopy(notification);
+	}
+
+	// Rename _id to id
+	_notification.id = _notification._id;
+	delete _notification._id;
+
+	// Rename notifier_id to user_id
+	_notification.user_id = _notification.notifier_id;
+	delete _notification.notifier_id;
+
+	const me = _notification.notifiee_id;
+	delete _notification.notifiee_id;
+
+	// Populate notifier
+	_notification.user = await packUser(_notification.user_id, me);
+
+	switch (_notification.type) {
+		case 'follow':
+			// nope
+			break;
+		case 'mention':
+		case 'reply':
+		case 'repost':
+		case 'quote':
+		case 'reaction':
+		case 'poll_vote':
+			// Populate post
+			_notification.post = await packPost(_notification.post_id, me);
+			break;
+		default:
+			console.error(`Unknown type: ${_notification.type}`);
+			break;
+	}
+
+	resolve(_notification);
+});
diff --git a/src/api/models/post-reaction.ts b/src/api/models/post-reaction.ts
index 282ae5bd2..568bfc89a 100644
--- a/src/api/models/post-reaction.ts
+++ b/src/api/models/post-reaction.ts
@@ -1,3 +1,48 @@
+import * as mongo from 'mongodb';
+import deepcopy = require('deepcopy');
 import db from '../../db/mongodb';
+import Reaction from './post-reaction';
+import { pack as packUser } from './user';
 
-export default db.get('post_reactions') as any; // fuck type definition
+const PostReaction = db.get<IPostReaction>('post_reactions');
+export default PostReaction;
+
+export interface IPostReaction {
+	_id: mongo.ObjectID;
+}
+
+/**
+ * Pack a reaction for API response
+ *
+ * @param {any} reaction
+ * @param {any} me?
+ * @return {Promise<any>}
+ */
+export const pack = (
+	reaction: any,
+	me?: any
+) => new Promise<any>(async (resolve, reject) => {
+	let _reaction: any;
+
+	// Populate the reaction if 'reaction' is ID
+	if (mongo.ObjectID.prototype.isPrototypeOf(reaction)) {
+		_reaction = await Reaction.findOne({
+			_id: reaction
+		});
+	} else if (typeof reaction === 'string') {
+		_reaction = await Reaction.findOne({
+			_id: new mongo.ObjectID(reaction)
+		});
+	} else {
+		_reaction = deepcopy(reaction);
+	}
+
+	// Rename _id to id
+	_reaction.id = _reaction._id;
+	delete _reaction._id;
+
+	// Populate user
+	_reaction.user = await packUser(_reaction.user_id, me);
+
+	resolve(_reaction);
+});
diff --git a/src/api/models/post.ts b/src/api/models/post.ts
index 7584ce182..ecc5e1a5e 100644
--- a/src/api/models/post.ts
+++ b/src/api/models/post.ts
@@ -1,8 +1,18 @@
 import * as mongo from 'mongodb';
-
+import deepcopy = require('deepcopy');
+import rap from '@prezzemolo/rap';
 import db from '../../db/mongodb';
+import { IUser, pack as packUser } from './user';
+import { pack as packApp } from './app';
+import { pack as packChannel } from './channel';
+import Vote from './poll-vote';
+import Reaction from './post-reaction';
+import { pack as packFile } from './drive-file';
+import parse from '../common/text';
 
-export default db.get('posts') as any; // fuck type definition
+const Post = db.get<IPost>('posts');
+
+export default Post;
 
 export function isValidText(text: string): boolean {
 	return text.length <= 1000 && text.trim() != '';
@@ -20,3 +30,178 @@ export type IPost = {
 	user_id: mongo.ObjectID;
 	app_id: mongo.ObjectID;
 };
+
+/**
+ * Pack a post for API response
+ *
+ * @param post target
+ * @param me? serializee
+ * @param options? serialize options
+ * @return response
+ */
+export const pack = async (
+	post: string | mongo.ObjectID | IPost,
+	me?: string | mongo.ObjectID | IUser,
+	options?: {
+		detail: boolean
+	}
+) => {
+	const opts = options || {
+		detail: true,
+	};
+
+	// Me
+	const meId: mongo.ObjectID = me
+		? mongo.ObjectID.prototype.isPrototypeOf(me)
+			? me as mongo.ObjectID
+			: typeof me === 'string'
+				? new mongo.ObjectID(me)
+				: (me as IUser)._id
+		: null;
+
+	let _post: any;
+
+	// Populate the post if 'post' is ID
+	if (mongo.ObjectID.prototype.isPrototypeOf(post)) {
+		_post = await Post.findOne({
+			_id: post
+		});
+	} else if (typeof post === 'string') {
+		_post = await Post.findOne({
+			_id: new mongo.ObjectID(post)
+		});
+	} else {
+		_post = deepcopy(post);
+	}
+
+	if (!_post) throw 'invalid post arg.';
+
+	const id = _post._id;
+
+	// Rename _id to id
+	_post.id = _post._id;
+	delete _post._id;
+
+	delete _post.mentions;
+
+	// Parse text
+	if (_post.text) {
+		_post.ast = parse(_post.text);
+	}
+
+	// Populate user
+	_post.user = packUser(_post.user_id, meId);
+
+	// Populate app
+	if (_post.app_id) {
+		_post.app = packApp(_post.app_id);
+	}
+
+	// Populate channel
+	if (_post.channel_id) {
+		_post.channel = packChannel(_post.channel_id);
+	}
+
+	// Populate media
+	if (_post.media_ids) {
+		_post.media = Promise.all(_post.media_ids.map(fileId =>
+			packFile(fileId)
+		));
+	}
+
+	// When requested a detailed post data
+	if (opts.detail) {
+		// Get previous post info
+		_post.prev = (async () => {
+			const prev = await Post.findOne({
+				user_id: _post.user_id,
+				_id: {
+					$lt: id
+				}
+			}, {
+				fields: {
+					_id: true
+				},
+				sort: {
+					_id: -1
+				}
+			});
+			return prev ? prev._id : null;
+		})();
+
+		// Get next post info
+		_post.next = (async () => {
+			const next = await Post.findOne({
+				user_id: _post.user_id,
+				_id: {
+					$gt: id
+				}
+			}, {
+				fields: {
+					_id: true
+				},
+				sort: {
+					_id: 1
+				}
+			});
+			return next ? next._id : null;
+		})();
+
+		if (_post.reply_id) {
+			// Populate reply to post
+			_post.reply = pack(_post.reply_id, meId, {
+				detail: false
+			});
+		}
+
+		if (_post.repost_id) {
+			// Populate repost
+			_post.repost = pack(_post.repost_id, meId, {
+				detail: _post.text == null
+			});
+		}
+
+		// Poll
+		if (meId && _post.poll) {
+			_post.poll = (async (poll) => {
+				const vote = await Vote
+					.findOne({
+						user_id: meId,
+						post_id: id
+					});
+
+				if (vote != null) {
+					const myChoice = poll.choices
+						.filter(c => c.id == vote.choice)[0];
+
+					myChoice.is_voted = true;
+				}
+
+				return poll;
+			})(_post.poll);
+		}
+
+		// Fetch my reaction
+		if (meId) {
+			_post.my_reaction = (async () => {
+				const reaction = await Reaction
+					.findOne({
+						user_id: meId,
+						post_id: id,
+						deleted_at: { $exists: false }
+					});
+
+				if (reaction) {
+					return reaction.reaction;
+				}
+
+				return null;
+			})();
+		}
+	}
+
+	// resolve promises in _post object
+	_post = await rap(_post);
+
+	return _post;
+};
diff --git a/src/api/models/signin.ts b/src/api/models/signin.ts
index 385a348f2..262c8707e 100644
--- a/src/api/models/signin.ts
+++ b/src/api/models/signin.ts
@@ -1,3 +1,29 @@
+import * as mongo from 'mongodb';
+import deepcopy = require('deepcopy');
 import db from '../../db/mongodb';
 
-export default db.get('signin') as any; // fuck type definition
+const Signin = db.get<ISignin>('signin');
+export default Signin;
+
+export interface ISignin {
+	_id: mongo.ObjectID;
+}
+
+/**
+ * Pack a signin record for API response
+ *
+ * @param {any} record
+ * @return {Promise<any>}
+ */
+export const pack = (
+	record: any
+) => new Promise<any>(async (resolve, reject) => {
+
+	const _record = deepcopy(record);
+
+	// Rename _id to id
+	_record.id = _record._id;
+	delete _record._id;
+
+	resolve(_record);
+});
diff --git a/src/api/models/user.ts b/src/api/models/user.ts
index 018979158..48a45ac2f 100644
--- a/src/api/models/user.ts
+++ b/src/api/models/user.ts
@@ -1,14 +1,19 @@
 import * as mongo from 'mongodb';
-
+import deepcopy = require('deepcopy');
+import rap from '@prezzemolo/rap';
 import db from '../../db/mongodb';
-import { IPost } from './post';
+import { IPost, pack as packPost } from './post';
+import Following from './following';
+import Mute from './mute';
+import getFriends from '../common/get-friends';
+import config from '../../conf';
 
-const collection = db.get('users');
+const User = db.get<IUser>('users');
 
-(collection as any).createIndex('username'); // fuck type definition
-(collection as any).createIndex('token'); // fuck type definition
+User.createIndex('username');
+User.createIndex('token');
 
-export default collection as any; // fuck type definition
+export default User;
 
 export function validateUsername(username: string): boolean {
 	return typeof username == 'string' && /^[a-zA-Z0-9\-]{3,20}$/.test(username);
@@ -83,3 +88,182 @@ export function init(user): IUser {
 	user.pinned_post_id = new mongo.ObjectID(user.pinned_post_id);
 	return user;
 }
+
+/**
+ * Pack a user for API response
+ *
+ * @param user target
+ * @param me? serializee
+ * @param options? serialize options
+ * @return Packed user
+ */
+export const pack = (
+	user: string | mongo.ObjectID | IUser,
+	me?: string | mongo.ObjectID | IUser,
+	options?: {
+		detail?: boolean,
+		includeSecrets?: boolean
+	}
+) => new Promise<any>(async (resolve, reject) => {
+
+	const opts = Object.assign({
+		detail: false,
+		includeSecrets: false
+	}, options);
+
+	let _user: any;
+
+	const fields = opts.detail ? {
+		settings: false
+	} : {
+		settings: false,
+		client_settings: false,
+		profile: false,
+		keywords: false,
+		domains: false
+	};
+
+	// Populate the user if 'user' is ID
+	if (mongo.ObjectID.prototype.isPrototypeOf(user)) {
+		_user = await User.findOne({
+			_id: user
+		}, { fields });
+	} else if (typeof user === 'string') {
+		_user = await User.findOne({
+			_id: new mongo.ObjectID(user)
+		}, { fields });
+	} else {
+		_user = deepcopy(user);
+	}
+
+	if (!_user) return reject('invalid user arg.');
+
+	// Me
+	const meId: mongo.ObjectID = me
+		? mongo.ObjectID.prototype.isPrototypeOf(me)
+			? me as mongo.ObjectID
+			: typeof me === 'string'
+				? new mongo.ObjectID(me)
+				: (me as IUser)._id
+		: null;
+
+	// Rename _id to id
+	_user.id = _user._id;
+	delete _user._id;
+
+	// Remove needless properties
+	delete _user.latest_post;
+
+	// Remove private properties
+	delete _user.password;
+	delete _user.token;
+	delete _user.two_factor_temp_secret;
+	delete _user.two_factor_secret;
+	delete _user.username_lower;
+	if (_user.twitter) {
+		delete _user.twitter.access_token;
+		delete _user.twitter.access_token_secret;
+	}
+	delete _user.line;
+
+	// Visible via only the official client
+	if (!opts.includeSecrets) {
+		delete _user.email;
+		delete _user.client_settings;
+	}
+
+	if (!opts.detail) {
+		delete _user.two_factor_enabled;
+	}
+
+	_user.avatar_url = _user.avatar_id != null
+		? `${config.drive_url}/${_user.avatar_id}`
+		: `${config.drive_url}/default-avatar.jpg`;
+
+	_user.banner_url = _user.banner_id != null
+		? `${config.drive_url}/${_user.banner_id}`
+		: null;
+
+	if (!meId || !meId.equals(_user.id) || !opts.detail) {
+		delete _user.avatar_id;
+		delete _user.banner_id;
+
+		delete _user.drive_capacity;
+	}
+
+	if (meId && !meId.equals(_user.id)) {
+		// Whether the user is following
+		_user.is_following = (async () => {
+			const follow = await Following.findOne({
+				follower_id: meId,
+				followee_id: _user.id,
+				deleted_at: { $exists: false }
+			});
+			return follow !== null;
+		})();
+
+		// Whether the user is followed
+		_user.is_followed = (async () => {
+			const follow2 = await Following.findOne({
+				follower_id: _user.id,
+				followee_id: meId,
+				deleted_at: { $exists: false }
+			});
+			return follow2 !== null;
+		})();
+
+		// Whether the user is muted
+		_user.is_muted = (async () => {
+			const mute = await Mute.findOne({
+				muter_id: meId,
+				mutee_id: _user.id,
+				deleted_at: { $exists: false }
+			});
+			return mute !== null;
+		})();
+	}
+
+	if (opts.detail) {
+		if (_user.pinned_post_id) {
+			// Populate pinned post
+			_user.pinned_post = packPost(_user.pinned_post_id, meId, {
+				detail: true
+			});
+		}
+
+		if (meId && !meId.equals(_user.id)) {
+			const myFollowingIds = await getFriends(meId);
+
+			// Get following you know count
+			_user.following_you_know_count = Following.count({
+				followee_id: { $in: myFollowingIds },
+				follower_id: _user.id,
+				deleted_at: { $exists: false }
+			});
+
+			// Get followers you know count
+			_user.followers_you_know_count = Following.count({
+				followee_id: _user.id,
+				follower_id: { $in: myFollowingIds },
+				deleted_at: { $exists: false }
+			});
+		}
+	}
+
+	// resolve promises in _user object
+	_user = await rap(_user);
+
+	resolve(_user);
+});
+
+/*
+function img(url) {
+	return {
+		thumbnail: {
+			large: `${url}`,
+			medium: '',
+			small: ''
+		}
+	};
+}
+*/
diff --git a/src/api/serializers/app.ts b/src/api/serializers/app.ts
deleted file mode 100644
index 9d1c46dca..000000000
--- a/src/api/serializers/app.ts
+++ /dev/null
@@ -1,83 +0,0 @@
-/**
- * Module dependencies
- */
-import * as mongo from 'mongodb';
-import deepcopy = require('deepcopy');
-import App from '../models/app';
-import AccessToken from '../models/access-token';
-import config from '../../conf';
-
-/**
- * Serialize an app
- *
- * @param {any} app
- * @param {any} me?
- * @param {any} options?
- * @return {Promise<any>}
- */
-export default (
-	app: any,
-	me?: any,
-	options?: {
-		includeSecret?: boolean,
-		includeProfileImageIds?: boolean
-	}
-) => new Promise<any>(async (resolve, reject) => {
-	const opts = options || {
-		includeSecret: false,
-		includeProfileImageIds: false
-	};
-
-	let _app: any;
-
-	// Populate the app if 'app' is ID
-	if (mongo.ObjectID.prototype.isPrototypeOf(app)) {
-		_app = await App.findOne({
-			_id: app
-		});
-	} else if (typeof app === 'string') {
-		_app = await App.findOne({
-			_id: new mongo.ObjectID(app)
-		});
-	} else {
-		_app = deepcopy(app);
-	}
-
-	// Me
-	if (me && !mongo.ObjectID.prototype.isPrototypeOf(me)) {
-		if (typeof me === 'string') {
-			me = new mongo.ObjectID(me);
-		} else {
-			me = me._id;
-		}
-	}
-
-	// Rename _id to id
-	_app.id = _app._id;
-	delete _app._id;
-
-	delete _app.name_id_lower;
-
-	// Visible by only owner
-	if (!opts.includeSecret) {
-		delete _app.secret;
-	}
-
-	_app.icon_url = _app.icon != null
-		? `${config.drive_url}/${_app.icon}`
-		: `${config.drive_url}/app-default.jpg`;
-
-	if (me) {
-		// 既に連携しているか
-		const exist = await AccessToken.count({
-			app_id: _app.id,
-			user_id: me,
-		}, {
-				limit: 1
-			});
-
-		_app.is_authorized = exist === 1;
-	}
-
-	resolve(_app);
-});
diff --git a/src/api/serializers/auth-session.ts b/src/api/serializers/auth-session.ts
deleted file mode 100644
index a9acf1243..000000000
--- a/src/api/serializers/auth-session.ts
+++ /dev/null
@@ -1,40 +0,0 @@
-/**
- * Module dependencies
- */
-import * as mongo from 'mongodb';
-import deepcopy = require('deepcopy');
-import serializeApp from './app';
-
-/**
- * Serialize an auth session
- *
- * @param {any} session
- * @param {any} me?
- * @return {Promise<any>}
- */
-export default (
-	session: any,
-	me?: any
-) => new Promise<any>(async (resolve, reject) => {
-	let _session: any;
-
-	// TODO: Populate session if it ID
-
-	_session = deepcopy(session);
-
-	// Me
-	if (me && !mongo.ObjectID.prototype.isPrototypeOf(me)) {
-		if (typeof me === 'string') {
-			me = new mongo.ObjectID(me);
-		} else {
-			me = me._id;
-		}
-	}
-
-	delete _session._id;
-
-	// Populate app
-	_session.app = await serializeApp(_session.app_id, me);
-
-	resolve(_session);
-});
diff --git a/src/api/serializers/channel.ts b/src/api/serializers/channel.ts
deleted file mode 100644
index 3cba39aa1..000000000
--- a/src/api/serializers/channel.ts
+++ /dev/null
@@ -1,66 +0,0 @@
-/**
- * Module dependencies
- */
-import * as mongo from 'mongodb';
-import deepcopy = require('deepcopy');
-import { IUser } from '../models/user';
-import { default as Channel, IChannel } from '../models/channel';
-import Watching from '../models/channel-watching';
-
-/**
- * Serialize a channel
- *
- * @param channel target
- * @param me? serializee
- * @return response
- */
-export default (
-	channel: string | mongo.ObjectID | IChannel,
-	me?: string | mongo.ObjectID | IUser
-) => new Promise<any>(async (resolve, reject) => {
-
-	let _channel: any;
-
-	// Populate the channel if 'channel' is ID
-	if (mongo.ObjectID.prototype.isPrototypeOf(channel)) {
-		_channel = await Channel.findOne({
-			_id: channel
-		});
-	} else if (typeof channel === 'string') {
-		_channel = await Channel.findOne({
-			_id: new mongo.ObjectID(channel)
-		});
-	} else {
-		_channel = deepcopy(channel);
-	}
-
-	// Rename _id to id
-	_channel.id = _channel._id;
-	delete _channel._id;
-
-	// Remove needless properties
-	delete _channel.user_id;
-
-	// Me
-	const meId: mongo.ObjectID = me
-	? mongo.ObjectID.prototype.isPrototypeOf(me)
-		? me as mongo.ObjectID
-		: typeof me === 'string'
-			? new mongo.ObjectID(me)
-			: (me as IUser)._id
-	: null;
-
-	if (me) {
-		//#region Watchしているかどうか
-		const watch = await Watching.findOne({
-			user_id: meId,
-			channel_id: _channel.id,
-			deleted_at: { $exists: false }
-		});
-
-		_channel.is_watching = watch !== null;
-		//#endregion
-	}
-
-	resolve(_channel);
-});
diff --git a/src/api/serializers/drive-file.ts b/src/api/serializers/drive-file.ts
deleted file mode 100644
index 003e09ee7..000000000
--- a/src/api/serializers/drive-file.ts
+++ /dev/null
@@ -1,78 +0,0 @@
-/**
- * Module dependencies
- */
-import * as mongo from 'mongodb';
-import DriveFile from '../models/drive-file';
-import serializeDriveFolder from './drive-folder';
-import serializeDriveTag from './drive-tag';
-import deepcopy = require('deepcopy');
-import config from '../../conf';
-
-/**
- * Serialize a drive file
- *
- * @param {any} file
- * @param {any} options?
- * @return {Promise<any>}
- */
-export default (
-	file: any,
-	options?: {
-		detail: boolean
-	}
-) => new Promise<any>(async (resolve, reject) => {
-	const opts = Object.assign({
-		detail: false
-	}, options);
-
-	let _file: any;
-
-	// Populate the file if 'file' is ID
-	if (mongo.ObjectID.prototype.isPrototypeOf(file)) {
-		_file = await DriveFile.findOne({
-			_id: file
-		});
-	} else if (typeof file === 'string') {
-		_file = await DriveFile.findOne({
-			_id: new mongo.ObjectID(file)
-		});
-	} else {
-		_file = deepcopy(file);
-	}
-
-	if (!_file) return reject('invalid file arg.');
-
-	// rendered target
-	let _target: any = {};
-
-	_target.id = _file._id;
-	_target.created_at = _file.uploadDate;
-	_target.name = _file.filename;
-	_target.type = _file.contentType;
-	_target.datasize = _file.length;
-	_target.md5 = _file.md5;
-
-	_target = Object.assign(_target, _file.metadata);
-
-	_target.url = `${config.drive_url}/${_target.id}/${encodeURIComponent(_target.name)}`;
-
-	if (_target.properties == null) _target.properties = {};
-
-	if (opts.detail) {
-		if (_target.folder_id) {
-			// Populate folder
-			_target.folder = await serializeDriveFolder(_target.folder_id, {
-				detail: true
-			});
-		}
-
-		if (_target.tags) {
-			// Populate tags
-			_target.tags = await _target.tags.map(async (tag: any) =>
-				await serializeDriveTag(tag)
-			);
-		}
-	}
-
-	resolve(_target);
-});
diff --git a/src/api/serializers/drive-folder.ts b/src/api/serializers/drive-folder.ts
deleted file mode 100644
index 6ebf454a2..000000000
--- a/src/api/serializers/drive-folder.ts
+++ /dev/null
@@ -1,64 +0,0 @@
-/**
- * Module dependencies
- */
-import * as mongo from 'mongodb';
-import DriveFolder from '../models/drive-folder';
-import DriveFile from '../models/drive-file';
-import deepcopy = require('deepcopy');
-
-/**
- * Serialize a drive folder
- *
- * @param {any} folder
- * @param {any} options?
- * @return {Promise<any>}
- */
-const self = (
-	folder: any,
-	options?: {
-		detail: boolean
-	}
-) => new Promise<any>(async (resolve, reject) => {
-	const opts = Object.assign({
-		detail: false
-	}, options);
-
-	let _folder: any;
-
-	// Populate the folder if 'folder' is ID
-	if (mongo.ObjectID.prototype.isPrototypeOf(folder)) {
-		_folder = await DriveFolder.findOne({ _id: folder });
-	} else if (typeof folder === 'string') {
-		_folder = await DriveFolder.findOne({ _id: new mongo.ObjectID(folder) });
-	} else {
-		_folder = deepcopy(folder);
-	}
-
-	// Rename _id to id
-	_folder.id = _folder._id;
-	delete _folder._id;
-
-	if (opts.detail) {
-		const childFoldersCount = await DriveFolder.count({
-			parent_id: _folder.id
-		});
-
-		const childFilesCount = await DriveFile.count({
-			'metadata.folder_id': _folder.id
-		});
-
-		_folder.folders_count = childFoldersCount;
-		_folder.files_count = childFilesCount;
-	}
-
-	if (opts.detail && _folder.parent_id) {
-		// Populate parent folder
-		_folder.parent = await self(_folder.parent_id, {
-			detail: true
-		});
-	}
-
-	resolve(_folder);
-});
-
-export default self;
diff --git a/src/api/serializers/drive-tag.ts b/src/api/serializers/drive-tag.ts
deleted file mode 100644
index 2f152381b..000000000
--- a/src/api/serializers/drive-tag.ts
+++ /dev/null
@@ -1,35 +0,0 @@
-/**
- * Module dependencies
- */
-import * as mongo from 'mongodb';
-import DriveTag from '../models/drive-tag';
-import deepcopy = require('deepcopy');
-
-/**
- * Serialize a drive tag
- *
- * @param {any} tag
- * @return {Promise<any>}
- */
-const self = (
-	tag: any
-) => new Promise<any>(async (resolve, reject) => {
-	let _tag: any;
-
-	// Populate the tag if 'tag' is ID
-	if (mongo.ObjectID.prototype.isPrototypeOf(tag)) {
-		_tag = await DriveTag.findOne({ _id: tag });
-	} else if (typeof tag === 'string') {
-		_tag = await DriveTag.findOne({ _id: new mongo.ObjectID(tag) });
-	} else {
-		_tag = deepcopy(tag);
-	}
-
-	// Rename _id to id
-	_tag.id = _tag._id;
-	delete _tag._id;
-
-	resolve(_tag);
-});
-
-export default self;
diff --git a/src/api/serializers/messaging-message.ts b/src/api/serializers/messaging-message.ts
deleted file mode 100644
index 4ab95e42a..000000000
--- a/src/api/serializers/messaging-message.ts
+++ /dev/null
@@ -1,68 +0,0 @@
-/**
- * Module dependencies
- */
-import * as mongo from 'mongodb';
-import deepcopy = require('deepcopy');
-import Message from '../models/messaging-message';
-import serializeUser from './user';
-import serializeDriveFile from './drive-file';
-import parse from '../common/text';
-
-/**
- * Serialize a message
- *
- * @param {any} message
- * @param {any} me?
- * @param {any} options?
- * @return {Promise<any>}
- */
-export default (
-	message: any,
-	me?: any,
-	options?: {
-		populateRecipient: boolean
-	}
-) => new Promise<any>(async (resolve, reject) => {
-	const opts = options || {
-		populateRecipient: true
-	};
-
-	let _message: any;
-
-	// Populate the message if 'message' is ID
-	if (mongo.ObjectID.prototype.isPrototypeOf(message)) {
-		_message = await Message.findOne({
-			_id: message
-		});
-	} else if (typeof message === 'string') {
-		_message = await Message.findOne({
-			_id: new mongo.ObjectID(message)
-		});
-	} else {
-		_message = deepcopy(message);
-	}
-
-	// Rename _id to id
-	_message.id = _message._id;
-	delete _message._id;
-
-	// Parse text
-	if (_message.text) {
-		_message.ast = parse(_message.text);
-	}
-
-	// Populate user
-	_message.user = await serializeUser(_message.user_id, me);
-
-	if (_message.file) {
-		// Populate file
-		_message.file = await serializeDriveFile(_message.file_id);
-	}
-
-	if (opts.populateRecipient) {
-		// Populate recipient
-		_message.recipient = await serializeUser(_message.recipient_id, me);
-	}
-
-	resolve(_message);
-});
diff --git a/src/api/serializers/notification.ts b/src/api/serializers/notification.ts
deleted file mode 100644
index ac919dc8b..000000000
--- a/src/api/serializers/notification.ts
+++ /dev/null
@@ -1,65 +0,0 @@
-/**
- * Module dependencies
- */
-import * as mongo from 'mongodb';
-import Notification from '../models/notification';
-import serializeUser from './user';
-import serializePost from './post';
-import deepcopy = require('deepcopy');
-
-/**
- * Serialize a notification
- *
- * @param {any} notification
- * @return {Promise<any>}
- */
-export default (notification: any) => new Promise<any>(async (resolve, reject) => {
-	let _notification: any;
-
-	// Populate the notification if 'notification' is ID
-	if (mongo.ObjectID.prototype.isPrototypeOf(notification)) {
-		_notification = await Notification.findOne({
-			_id: notification
-		});
-	} else if (typeof notification === 'string') {
-		_notification = await Notification.findOne({
-			_id: new mongo.ObjectID(notification)
-		});
-	} else {
-		_notification = deepcopy(notification);
-	}
-
-	// Rename _id to id
-	_notification.id = _notification._id;
-	delete _notification._id;
-
-	// Rename notifier_id to user_id
-	_notification.user_id = _notification.notifier_id;
-	delete _notification.notifier_id;
-
-	const me = _notification.notifiee_id;
-	delete _notification.notifiee_id;
-
-	// Populate notifier
-	_notification.user = await serializeUser(_notification.user_id, me);
-
-	switch (_notification.type) {
-		case 'follow':
-			// nope
-			break;
-		case 'mention':
-		case 'reply':
-		case 'repost':
-		case 'quote':
-		case 'reaction':
-		case 'poll_vote':
-			// Populate post
-			_notification.post = await serializePost(_notification.post_id, me);
-			break;
-		default:
-			console.error(`Unknown type: ${_notification.type}`);
-			break;
-	}
-
-	resolve(_notification);
-});
diff --git a/src/api/serializers/post-reaction.ts b/src/api/serializers/post-reaction.ts
deleted file mode 100644
index b8807a741..000000000
--- a/src/api/serializers/post-reaction.ts
+++ /dev/null
@@ -1,43 +0,0 @@
-/**
- * Module dependencies
- */
-import * as mongo from 'mongodb';
-import deepcopy = require('deepcopy');
-import Reaction from '../models/post-reaction';
-import serializeUser from './user';
-
-/**
- * Serialize a reaction
- *
- * @param {any} reaction
- * @param {any} me?
- * @return {Promise<any>}
- */
-export default (
-	reaction: any,
-	me?: any
-) => new Promise<any>(async (resolve, reject) => {
-	let _reaction: any;
-
-	// Populate the reaction if 'reaction' is ID
-	if (mongo.ObjectID.prototype.isPrototypeOf(reaction)) {
-		_reaction = await Reaction.findOne({
-			_id: reaction
-		});
-	} else if (typeof reaction === 'string') {
-		_reaction = await Reaction.findOne({
-			_id: new mongo.ObjectID(reaction)
-		});
-	} else {
-		_reaction = deepcopy(reaction);
-	}
-
-	// Rename _id to id
-	_reaction.id = _reaction._id;
-	delete _reaction._id;
-
-	// Populate user
-	_reaction.user = await serializeUser(_reaction.user_id, me);
-
-	resolve(_reaction);
-});
diff --git a/src/api/serializers/post.ts b/src/api/serializers/post.ts
deleted file mode 100644
index 03fd12077..000000000
--- a/src/api/serializers/post.ts
+++ /dev/null
@@ -1,192 +0,0 @@
-/**
- * Module dependencies
- */
-import * as mongo from 'mongodb';
-import deepcopy = require('deepcopy');
-import { default as Post, IPost } from '../models/post';
-import Reaction from '../models/post-reaction';
-import { IUser } from '../models/user';
-import Vote from '../models/poll-vote';
-import serializeApp from './app';
-import serializeChannel from './channel';
-import serializeUser from './user';
-import serializeDriveFile from './drive-file';
-import parse from '../common/text';
-import rap from '@prezzemolo/rap';
-
-/**
- * Serialize a post
- *
- * @param post target
- * @param me? serializee
- * @param options? serialize options
- * @return response
- */
-const self = async (
-	post: string | mongo.ObjectID | IPost,
-	me?: string | mongo.ObjectID | IUser,
-	options?: {
-		detail: boolean
-	}
-) => {
-	const opts = options || {
-		detail: true,
-	};
-
-	// Me
-	const meId: mongo.ObjectID = me
-		? mongo.ObjectID.prototype.isPrototypeOf(me)
-			? me as mongo.ObjectID
-			: typeof me === 'string'
-				? new mongo.ObjectID(me)
-				: (me as IUser)._id
-		: null;
-
-	let _post: any;
-
-	// Populate the post if 'post' is ID
-	if (mongo.ObjectID.prototype.isPrototypeOf(post)) {
-		_post = await Post.findOne({
-			_id: post
-		});
-	} else if (typeof post === 'string') {
-		_post = await Post.findOne({
-			_id: new mongo.ObjectID(post)
-		});
-	} else {
-		_post = deepcopy(post);
-	}
-
-	if (!_post) throw 'invalid post arg.';
-
-	const id = _post._id;
-
-	// Rename _id to id
-	_post.id = _post._id;
-	delete _post._id;
-
-	delete _post.mentions;
-
-	// Parse text
-	if (_post.text) {
-		_post.ast = parse(_post.text);
-	}
-
-	// Populate user
-	_post.user = serializeUser(_post.user_id, meId);
-
-	// Populate app
-	if (_post.app_id) {
-		_post.app = serializeApp(_post.app_id);
-	}
-
-	// Populate channel
-	if (_post.channel_id) {
-		_post.channel = serializeChannel(_post.channel_id);
-	}
-
-	// Populate media
-	if (_post.media_ids) {
-		_post.media = Promise.all(_post.media_ids.map(fileId =>
-			serializeDriveFile(fileId)
-		));
-	}
-
-	// When requested a detailed post data
-	if (opts.detail) {
-		// Get previous post info
-		_post.prev = (async () => {
-			const prev = await Post.findOne({
-				user_id: _post.user_id,
-				_id: {
-					$lt: id
-				}
-			}, {
-				fields: {
-					_id: true
-				},
-				sort: {
-					_id: -1
-				}
-			});
-			return prev ? prev._id : null;
-		})();
-
-		// Get next post info
-		_post.next = (async () => {
-			const next = await Post.findOne({
-				user_id: _post.user_id,
-				_id: {
-					$gt: id
-				}
-			}, {
-				fields: {
-					_id: true
-				},
-				sort: {
-					_id: 1
-				}
-			});
-			return next ? next._id : null;
-		})();
-
-		if (_post.reply_id) {
-			// Populate reply to post
-			_post.reply = self(_post.reply_id, meId, {
-				detail: false
-			});
-		}
-
-		if (_post.repost_id) {
-			// Populate repost
-			_post.repost = self(_post.repost_id, meId, {
-				detail: _post.text == null
-			});
-		}
-
-		// Poll
-		if (meId && _post.poll) {
-			_post.poll = (async (poll) => {
-				const vote = await Vote
-					.findOne({
-						user_id: meId,
-						post_id: id
-					});
-
-				if (vote != null) {
-					const myChoice = poll.choices
-						.filter(c => c.id == vote.choice)[0];
-
-					myChoice.is_voted = true;
-				}
-
-				return poll;
-			})(_post.poll);
-		}
-
-		// Fetch my reaction
-		if (meId) {
-			_post.my_reaction = (async () => {
-				const reaction = await Reaction
-					.findOne({
-						user_id: meId,
-						post_id: id,
-						deleted_at: { $exists: false }
-					});
-
-				if (reaction) {
-					return reaction.reaction;
-				}
-
-				return null;
-			})();
-		}
-	}
-
-	// resolve promises in _post object
-	_post = await rap(_post);
-
-	return _post;
-};
-
-export default self;
diff --git a/src/api/serializers/signin.ts b/src/api/serializers/signin.ts
deleted file mode 100644
index 406806767..000000000
--- a/src/api/serializers/signin.ts
+++ /dev/null
@@ -1,23 +0,0 @@
-/**
- * Module dependencies
- */
-import deepcopy = require('deepcopy');
-
-/**
- * Serialize a signin record
- *
- * @param {any} record
- * @return {Promise<any>}
- */
-export default (
-	record: any
-) => new Promise<any>(async (resolve, reject) => {
-
-	const _record = deepcopy(record);
-
-	// Rename _id to id
-	_record.id = _record._id;
-	delete _record._id;
-
-	resolve(_record);
-});
diff --git a/src/api/serializers/user.ts b/src/api/serializers/user.ts
deleted file mode 100644
index ac157097a..000000000
--- a/src/api/serializers/user.ts
+++ /dev/null
@@ -1,190 +0,0 @@
-/**
- * Module dependencies
- */
-import * as mongo from 'mongodb';
-import deepcopy = require('deepcopy');
-import { default as User, IUser } from '../models/user';
-import serializePost from './post';
-import Following from '../models/following';
-import Mute from '../models/mute';
-import getFriends from '../common/get-friends';
-import config from '../../conf';
-import rap from '@prezzemolo/rap';
-
-/**
- * Serialize a user
- *
- * @param user target
- * @param me? serializee
- * @param options? serialize options
- * @return response
- */
-export default (
-	user: string | mongo.ObjectID | IUser,
-	me?: string | mongo.ObjectID | IUser,
-	options?: {
-		detail?: boolean,
-		includeSecrets?: boolean
-	}
-) => new Promise<any>(async (resolve, reject) => {
-
-	const opts = Object.assign({
-		detail: false,
-		includeSecrets: false
-	}, options);
-
-	let _user: any;
-
-	const fields = opts.detail ? {
-		settings: false
-	} : {
-		settings: false,
-		client_settings: false,
-		profile: false,
-		keywords: false,
-		domains: false
-	};
-
-	// Populate the user if 'user' is ID
-	if (mongo.ObjectID.prototype.isPrototypeOf(user)) {
-		_user = await User.findOne({
-			_id: user
-		}, { fields });
-	} else if (typeof user === 'string') {
-		_user = await User.findOne({
-			_id: new mongo.ObjectID(user)
-		}, { fields });
-	} else {
-		_user = deepcopy(user);
-	}
-
-	if (!_user) return reject('invalid user arg.');
-
-	// Me
-	const meId: mongo.ObjectID = me
-		? mongo.ObjectID.prototype.isPrototypeOf(me)
-			? me as mongo.ObjectID
-			: typeof me === 'string'
-				? new mongo.ObjectID(me)
-				: (me as IUser)._id
-		: null;
-
-	// Rename _id to id
-	_user.id = _user._id;
-	delete _user._id;
-
-	// Remove needless properties
-	delete _user.latest_post;
-
-	// Remove private properties
-	delete _user.password;
-	delete _user.token;
-	delete _user.two_factor_temp_secret;
-	delete _user.two_factor_secret;
-	delete _user.username_lower;
-	if (_user.twitter) {
-		delete _user.twitter.access_token;
-		delete _user.twitter.access_token_secret;
-	}
-	delete _user.line;
-
-	// Visible via only the official client
-	if (!opts.includeSecrets) {
-		delete _user.email;
-		delete _user.client_settings;
-	}
-
-	if (!opts.detail) {
-		delete _user.two_factor_enabled;
-	}
-
-	_user.avatar_url = _user.avatar_id != null
-		? `${config.drive_url}/${_user.avatar_id}`
-		: `${config.drive_url}/default-avatar.jpg`;
-
-	_user.banner_url = _user.banner_id != null
-		? `${config.drive_url}/${_user.banner_id}`
-		: null;
-
-	if (!meId || !meId.equals(_user.id) || !opts.detail) {
-		delete _user.avatar_id;
-		delete _user.banner_id;
-
-		delete _user.drive_capacity;
-	}
-
-	if (meId && !meId.equals(_user.id)) {
-		// Whether the user is following
-		_user.is_following = (async () => {
-			const follow = await Following.findOne({
-				follower_id: meId,
-				followee_id: _user.id,
-				deleted_at: { $exists: false }
-			});
-			return follow !== null;
-		})();
-
-		// Whether the user is followed
-		_user.is_followed = (async () => {
-			const follow2 = await Following.findOne({
-				follower_id: _user.id,
-				followee_id: meId,
-				deleted_at: { $exists: false }
-			});
-			return follow2 !== null;
-		})();
-
-		// Whether the user is muted
-		_user.is_muted = (async () => {
-			const mute = await Mute.findOne({
-				muter_id: meId,
-				mutee_id: _user.id,
-				deleted_at: { $exists: false }
-			});
-			return mute !== null;
-		})();
-	}
-
-	if (opts.detail) {
-		if (_user.pinned_post_id) {
-			// Populate pinned post
-			_user.pinned_post = serializePost(_user.pinned_post_id, meId, {
-				detail: true
-			});
-		}
-
-		if (meId && !meId.equals(_user.id)) {
-			const myFollowingIds = await getFriends(meId);
-
-			// Get following you know count
-			_user.following_you_know_count = Following.count({
-				followee_id: { $in: myFollowingIds },
-				follower_id: _user.id,
-				deleted_at: { $exists: false }
-			});
-
-			// Get followers you know count
-			_user.followers_you_know_count = Following.count({
-				followee_id: _user.id,
-				follower_id: { $in: myFollowingIds },
-				deleted_at: { $exists: false }
-			});
-		}
-	}
-
-	// resolve promises in _user object
-	_user = await rap(_user);
-
-	resolve(_user);
-});
-/*
-function img(url) {
-	return {
-		thumbnail: {
-			large: `${url}`,
-			medium: '',
-			small: ''
-		}
-	};
-}
-*/

From 718060dc855e09f270b8e19c089ed3c3743665e0 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 2 Feb 2018 08:21:30 +0900
Subject: [PATCH 184/186] wip

---
 src/api/common/add-file-to-drive.ts                    |  4 ++--
 src/api/common/notify.ts                               |  6 +++---
 src/api/endpoints/app/create.ts                        |  4 ++--
 src/api/endpoints/app/show.ts                          |  4 ++--
 src/api/endpoints/auth/session/show.ts                 |  4 ++--
 src/api/endpoints/auth/session/userkey.ts              |  4 ++--
 src/api/endpoints/channels.ts                          |  4 ++--
 src/api/endpoints/channels/create.ts                   |  4 ++--
 src/api/endpoints/channels/posts.ts                    |  4 ++--
 src/api/endpoints/channels/show.ts                     |  4 ++--
 src/api/endpoints/drive/files.ts                       |  4 ++--
 src/api/endpoints/drive/files/create.ts                |  4 ++--
 src/api/endpoints/drive/files/find.ts                  |  4 ++--
 src/api/endpoints/drive/files/show.ts                  |  4 ++--
 src/api/endpoints/drive/files/update.ts                |  4 ++--
 src/api/endpoints/drive/files/upload_from_url.ts       |  4 ++--
 src/api/endpoints/drive/folders.ts                     |  4 ++--
 src/api/endpoints/drive/folders/create.ts              |  4 ++--
 src/api/endpoints/drive/folders/find.ts                |  4 ++--
 src/api/endpoints/drive/folders/show.ts                |  4 ++--
 src/api/endpoints/drive/folders/update.ts              |  4 ++--
 src/api/endpoints/drive/stream.ts                      |  4 ++--
 src/api/endpoints/i.ts                                 |  4 ++--
 src/api/endpoints/i/authorized_apps.ts                 |  4 ++--
 src/api/endpoints/i/favorites.ts                       |  4 ++--
 src/api/endpoints/i/notifications.ts                   |  4 ++--
 src/api/endpoints/i/pin.ts                             |  4 ++--
 src/api/endpoints/i/signin_history.ts                  |  4 ++--
 src/api/endpoints/i/update.ts                          |  4 ++--
 src/api/endpoints/messaging/history.ts                 |  4 ++--
 src/api/endpoints/messaging/messages.ts                |  4 ++--
 src/api/endpoints/messaging/messages/create.ts         |  4 ++--
 src/api/endpoints/mute/list.ts                         |  4 ++--
 src/api/endpoints/my/apps.ts                           |  4 ++--
 src/api/endpoints/posts.ts                             |  4 ++--
 src/api/endpoints/posts/context.ts                     |  4 ++--
 src/api/endpoints/posts/create.ts                      |  4 ++--
 src/api/endpoints/posts/mentions.ts                    |  4 ++--
 src/api/endpoints/posts/polls/recommendation.ts        |  4 ++--
 src/api/endpoints/posts/reactions.ts                   |  4 ++--
 src/api/endpoints/posts/replies.ts                     |  4 ++--
 src/api/endpoints/posts/reposts.ts                     |  4 ++--
 src/api/endpoints/posts/search.ts                      |  4 ++--
 src/api/endpoints/posts/show.ts                        |  4 ++--
 src/api/endpoints/posts/timeline.ts                    |  4 ++--
 src/api/endpoints/posts/trend.ts                       |  4 ++--
 src/api/endpoints/users.ts                             |  4 ++--
 src/api/endpoints/users/followers.ts                   |  4 ++--
 src/api/endpoints/users/following.ts                   |  4 ++--
 .../endpoints/users/get_frequently_replied_users.ts    |  4 ++--
 src/api/endpoints/users/posts.ts                       |  4 ++--
 src/api/endpoints/users/recommendation.ts              |  4 ++--
 src/api/endpoints/users/search.ts                      |  4 ++--
 src/api/endpoints/users/search_by_username.ts          |  4 ++--
 src/api/endpoints/users/show.ts                        |  4 ++--
 src/api/models/drive-file.ts                           | 10 ++++++++--
 src/api/models/drive-folder.ts                         |  2 ++
 src/api/models/messaging-message.ts                    |  5 +++++
 src/api/private/signin.ts                              |  4 ++--
 src/api/private/signup.ts                              |  4 ++--
 src/api/service/twitter.ts                             |  4 ++--
 61 files changed, 132 insertions(+), 119 deletions(-)

diff --git a/src/api/common/add-file-to-drive.ts b/src/api/common/add-file-to-drive.ts
index 23cbc44e6..1ee455c09 100644
--- a/src/api/common/add-file-to-drive.ts
+++ b/src/api/common/add-file-to-drive.ts
@@ -12,7 +12,7 @@ import prominence = require('prominence');
 
 import DriveFile, { getGridFSBucket } from '../models/drive-file';
 import DriveFolder from '../models/drive-folder';
-import serialize from '../serializers/drive-file';
+import { pack } from '../models/drive-file';
 import event, { publishDriveStream } from '../event';
 import config from '../../conf';
 
@@ -282,7 +282,7 @@ export default (user: any, file: string | stream.Readable, ...args) => new Promi
 		log(`drive file has been created ${file._id}`);
 		resolve(file);
 
-		serialize(file).then(serializedFile => {
+		pack(file).then(serializedFile => {
 			// Publish drive_file_created event
 			event(user._id, 'drive_file_created', serializedFile);
 			publishDriveStream(user._id, 'file_created', serializedFile);
diff --git a/src/api/common/notify.ts b/src/api/common/notify.ts
index 2b79416a3..ae5669b84 100644
--- a/src/api/common/notify.ts
+++ b/src/api/common/notify.ts
@@ -2,7 +2,7 @@ import * as mongo from 'mongodb';
 import Notification from '../models/notification';
 import Mute from '../models/mute';
 import event from '../event';
-import serialize from '../serializers/notification';
+import { pack } from '../models/notification';
 
 export default (
 	notifiee: mongo.ObjectID,
@@ -27,7 +27,7 @@ export default (
 
 	// Publish notification event
 	event(notifiee, 'notification',
-		await serialize(notification));
+		await pack(notification));
 
 	// 3秒経っても(今回作成した)通知が既読にならなかったら「未読の通知がありますよ」イベントを発行する
 	setTimeout(async () => {
@@ -44,7 +44,7 @@ export default (
 			}
 			//#endregion
 
-			event(notifiee, 'unread_notification', await serialize(notification));
+			event(notifiee, 'unread_notification', await pack(notification));
 		}
 	}, 3000);
 });
diff --git a/src/api/endpoints/app/create.ts b/src/api/endpoints/app/create.ts
index ca684de02..320163ebd 100644
--- a/src/api/endpoints/app/create.ts
+++ b/src/api/endpoints/app/create.ts
@@ -5,7 +5,7 @@ import rndstr from 'rndstr';
 import $ from 'cafy';
 import App from '../../models/app';
 import { isValidNameId } from '../../models/app';
-import serialize from '../../serializers/app';
+import { pack } from '../../models/app';
 
 /**
  * @swagger
@@ -106,5 +106,5 @@ module.exports = async (params, user) => new Promise(async (res, rej) => {
 	});
 
 	// Response
-	res(await serialize(app));
+	res(await pack(app));
 });
diff --git a/src/api/endpoints/app/show.ts b/src/api/endpoints/app/show.ts
index 054aab859..a3ef24717 100644
--- a/src/api/endpoints/app/show.ts
+++ b/src/api/endpoints/app/show.ts
@@ -3,7 +3,7 @@
  */
 import $ from 'cafy';
 import App from '../../models/app';
-import serialize from '../../serializers/app';
+import { pack } from '../../models/app';
 
 /**
  * @swagger
@@ -67,7 +67,7 @@ module.exports = (params, user, _, isSecure) => new Promise(async (res, rej) =>
 	}
 
 	// Send response
-	res(await serialize(app, user, {
+	res(await pack(app, user, {
 		includeSecret: isSecure && app.user_id.equals(user._id)
 	}));
 });
diff --git a/src/api/endpoints/auth/session/show.ts b/src/api/endpoints/auth/session/show.ts
index ede8a6763..1fe3b873f 100644
--- a/src/api/endpoints/auth/session/show.ts
+++ b/src/api/endpoints/auth/session/show.ts
@@ -3,7 +3,7 @@
  */
 import $ from 'cafy';
 import AuthSess from '../../../models/auth-session';
-import serialize from '../../../serializers/auth-session';
+import { pack } from '../../../models/auth-session';
 
 /**
  * @swagger
@@ -67,5 +67,5 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 	}
 
 	// Response
-	res(await serialize(session, user));
+	res(await pack(session, user));
 });
diff --git a/src/api/endpoints/auth/session/userkey.ts b/src/api/endpoints/auth/session/userkey.ts
index afd3250b0..fc989bf8c 100644
--- a/src/api/endpoints/auth/session/userkey.ts
+++ b/src/api/endpoints/auth/session/userkey.ts
@@ -5,7 +5,7 @@ import $ from 'cafy';
 import App from '../../../models/app';
 import AuthSess from '../../../models/auth-session';
 import AccessToken from '../../../models/access-token';
-import serialize from '../../../serializers/user';
+import { pack } from '../../../models/user';
 
 /**
  * @swagger
@@ -102,7 +102,7 @@ module.exports = (params) => new Promise(async (res, rej) => {
 	// Response
 	res({
 		access_token: accessToken.token,
-		user: await serialize(session.user_id, null, {
+		user: await pack(session.user_id, null, {
 			detail: true
 		})
 	});
diff --git a/src/api/endpoints/channels.ts b/src/api/endpoints/channels.ts
index 14817d9bd..92dcee83d 100644
--- a/src/api/endpoints/channels.ts
+++ b/src/api/endpoints/channels.ts
@@ -3,7 +3,7 @@
  */
 import $ from 'cafy';
 import Channel from '../models/channel';
-import serialize from '../serializers/channel';
+import { pack } from '../models/channel';
 
 /**
  * Get all channels
@@ -55,5 +55,5 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
 
 	// Serialize
 	res(await Promise.all(channels.map(async channel =>
-		await serialize(channel, me))));
+		await pack(channel, me))));
 });
diff --git a/src/api/endpoints/channels/create.ts b/src/api/endpoints/channels/create.ts
index a8d7c29dc..695b4515b 100644
--- a/src/api/endpoints/channels/create.ts
+++ b/src/api/endpoints/channels/create.ts
@@ -4,7 +4,7 @@
 import $ from 'cafy';
 import Channel from '../../models/channel';
 import Watching from '../../models/channel-watching';
-import serialize from '../../serializers/channel';
+import { pack } from '../../models/channel';
 
 /**
  * Create a channel
@@ -28,7 +28,7 @@ module.exports = async (params, user) => new Promise(async (res, rej) => {
 	});
 
 	// Response
-	res(await serialize(channel));
+	res(await pack(channel));
 
 	// Create Watching
 	await Watching.insert({
diff --git a/src/api/endpoints/channels/posts.ts b/src/api/endpoints/channels/posts.ts
index 9c2d607ed..3feee51f7 100644
--- a/src/api/endpoints/channels/posts.ts
+++ b/src/api/endpoints/channels/posts.ts
@@ -4,7 +4,7 @@
 import $ from 'cafy';
 import { default as Channel, IChannel } from '../../models/channel';
 import Post from '../../models/post';
-import serialize from '../../serializers/post';
+import { pack } from '../../models/post';
 
 /**
  * Show a posts of a channel
@@ -74,6 +74,6 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 
 	// Serialize
 	res(await Promise.all(posts.map(async (post) =>
-		await serialize(post, user)
+		await pack(post, user)
 	)));
 });
diff --git a/src/api/endpoints/channels/show.ts b/src/api/endpoints/channels/show.ts
index 8861e5459..89c48379a 100644
--- a/src/api/endpoints/channels/show.ts
+++ b/src/api/endpoints/channels/show.ts
@@ -3,7 +3,7 @@
  */
 import $ from 'cafy';
 import { default as Channel, IChannel } from '../../models/channel';
-import serialize from '../../serializers/channel';
+import { pack } from '../../models/channel';
 
 /**
  * Show a channel
@@ -27,5 +27,5 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 	}
 
 	// Serialize
-	res(await serialize(channel, user));
+	res(await pack(channel, user));
 });
diff --git a/src/api/endpoints/drive/files.ts b/src/api/endpoints/drive/files.ts
index 3d5f81339..3bd80e728 100644
--- a/src/api/endpoints/drive/files.ts
+++ b/src/api/endpoints/drive/files.ts
@@ -3,7 +3,7 @@
  */
 import $ from 'cafy';
 import DriveFile from '../../models/drive-file';
-import serialize from '../../serializers/drive-file';
+import { pack } from '../../models/drive-file';
 
 /**
  * Get drive files
@@ -69,6 +69,6 @@ module.exports = async (params, user, app) => {
 		});
 
 	// Serialize
-	const _files = await Promise.all(files.map(file => serialize(file)));
+	const _files = await Promise.all(files.map(file => pack(file)));
 	return _files;
 };
diff --git a/src/api/endpoints/drive/files/create.ts b/src/api/endpoints/drive/files/create.ts
index 437348a1e..6fa76d7e9 100644
--- a/src/api/endpoints/drive/files/create.ts
+++ b/src/api/endpoints/drive/files/create.ts
@@ -3,7 +3,7 @@
  */
 import $ from 'cafy';
 import { validateFileName } from '../../../models/drive-file';
-import serialize from '../../../serializers/drive-file';
+import { pack } from '../../../models/drive-file';
 import create from '../../../common/add-file-to-drive';
 
 /**
@@ -43,7 +43,7 @@ module.exports = async (file, params, user): Promise<any> => {
 		const driveFile = await create(user, file.path, name, null, folderId);
 
 		// Serialize
-		return serialize(driveFile);
+		return pack(driveFile);
 	} catch (e) {
 		console.error(e);
 
diff --git a/src/api/endpoints/drive/files/find.ts b/src/api/endpoints/drive/files/find.ts
index a1cdf1643..571aba81f 100644
--- a/src/api/endpoints/drive/files/find.ts
+++ b/src/api/endpoints/drive/files/find.ts
@@ -3,7 +3,7 @@
  */
 import $ from 'cafy';
 import DriveFile from '../../../models/drive-file';
-import serialize from '../../../serializers/drive-file';
+import { pack } from '../../../models/drive-file';
 
 /**
  * Find a file(s)
@@ -31,5 +31,5 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 
 	// Serialize
 	res(await Promise.all(files.map(async file =>
-		await serialize(file))));
+		await pack(file))));
 });
diff --git a/src/api/endpoints/drive/files/show.ts b/src/api/endpoints/drive/files/show.ts
index 3c7cf774f..00f69f141 100644
--- a/src/api/endpoints/drive/files/show.ts
+++ b/src/api/endpoints/drive/files/show.ts
@@ -3,7 +3,7 @@
  */
 import $ from 'cafy';
 import DriveFile from '../../../models/drive-file';
-import serialize from '../../../serializers/drive-file';
+import { pack } from '../../../models/drive-file';
 
 /**
  * Show a file
@@ -29,7 +29,7 @@ module.exports = async (params, user) => {
 	}
 
 	// Serialize
-	const _file = await serialize(file, {
+	const _file = await pack(file, {
 		detail: true
 	});
 
diff --git a/src/api/endpoints/drive/files/update.ts b/src/api/endpoints/drive/files/update.ts
index f39a420d6..9ef8215b1 100644
--- a/src/api/endpoints/drive/files/update.ts
+++ b/src/api/endpoints/drive/files/update.ts
@@ -5,7 +5,7 @@ import $ from 'cafy';
 import DriveFolder from '../../../models/drive-folder';
 import DriveFile from '../../../models/drive-file';
 import { validateFileName } from '../../../models/drive-file';
-import serialize from '../../../serializers/drive-file';
+import { pack } from '../../../models/drive-file';
 import { publishDriveStream } from '../../../event';
 
 /**
@@ -67,7 +67,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 	});
 
 	// Serialize
-	const fileObj = await serialize(file);
+	const fileObj = await pack(file);
 
 	// Response
 	res(fileObj);
diff --git a/src/api/endpoints/drive/files/upload_from_url.ts b/src/api/endpoints/drive/files/upload_from_url.ts
index 519e0bdf6..f0398bfc5 100644
--- a/src/api/endpoints/drive/files/upload_from_url.ts
+++ b/src/api/endpoints/drive/files/upload_from_url.ts
@@ -4,7 +4,7 @@
 import * as URL from 'url';
 import $ from 'cafy';
 import { validateFileName } from '../../../models/drive-file';
-import serialize from '../../../serializers/drive-file';
+import { pack } from '../../../models/drive-file';
 import create from '../../../common/add-file-to-drive';
 import * as debug from 'debug';
 import * as tmp from 'tmp';
@@ -63,5 +63,5 @@ module.exports = async (params, user): Promise<any> => {
 		if (e) log(e.stack);
 	});
 
-	return serialize(driveFile);
+	return pack(driveFile);
 };
diff --git a/src/api/endpoints/drive/folders.ts b/src/api/endpoints/drive/folders.ts
index 7944e2c6a..e650fb74a 100644
--- a/src/api/endpoints/drive/folders.ts
+++ b/src/api/endpoints/drive/folders.ts
@@ -3,7 +3,7 @@
  */
 import $ from 'cafy';
 import DriveFolder from '../../models/drive-folder';
-import serialize from '../../serializers/drive-folder';
+import { pack } from '../../models/drive-folder';
 
 /**
  * Get drive folders
@@ -63,5 +63,5 @@ module.exports = (params, user, app) => new Promise(async (res, rej) => {
 
 	// Serialize
 	res(await Promise.all(folders.map(async folder =>
-		await serialize(folder))));
+		await pack(folder))));
 });
diff --git a/src/api/endpoints/drive/folders/create.ts b/src/api/endpoints/drive/folders/create.ts
index be847b215..1953c09ee 100644
--- a/src/api/endpoints/drive/folders/create.ts
+++ b/src/api/endpoints/drive/folders/create.ts
@@ -4,7 +4,7 @@
 import $ from 'cafy';
 import DriveFolder from '../../../models/drive-folder';
 import { isValidFolderName } from '../../../models/drive-folder';
-import serialize from '../../../serializers/drive-folder';
+import { pack } from '../../../models/drive-folder';
 import { publishDriveStream } from '../../../event';
 
 /**
@@ -47,7 +47,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 	});
 
 	// Serialize
-	const folderObj = await serialize(folder);
+	const folderObj = await pack(folder);
 
 	// Response
 	res(folderObj);
diff --git a/src/api/endpoints/drive/folders/find.ts b/src/api/endpoints/drive/folders/find.ts
index a5eb8e015..caad45d74 100644
--- a/src/api/endpoints/drive/folders/find.ts
+++ b/src/api/endpoints/drive/folders/find.ts
@@ -3,7 +3,7 @@
  */
 import $ from 'cafy';
 import DriveFolder from '../../../models/drive-folder';
-import serialize from '../../../serializers/drive-folder';
+import { pack } from '../../../models/drive-folder';
 
 /**
  * Find a folder(s)
@@ -30,5 +30,5 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 		});
 
 	// Serialize
-	res(await Promise.all(folders.map(folder => serialize(folder))));
+	res(await Promise.all(folders.map(folder => pack(folder))));
 });
diff --git a/src/api/endpoints/drive/folders/show.ts b/src/api/endpoints/drive/folders/show.ts
index 9b1c04ca3..fd3061ca5 100644
--- a/src/api/endpoints/drive/folders/show.ts
+++ b/src/api/endpoints/drive/folders/show.ts
@@ -3,7 +3,7 @@
  */
 import $ from 'cafy';
 import DriveFolder from '../../../models/drive-folder';
-import serialize from '../../../serializers/drive-folder';
+import { pack } from '../../../models/drive-folder';
 
 /**
  * Show a folder
@@ -29,7 +29,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 	}
 
 	// Serialize
-	res(await serialize(folder, {
+	res(await pack(folder, {
 		detail: true
 	}));
 });
diff --git a/src/api/endpoints/drive/folders/update.ts b/src/api/endpoints/drive/folders/update.ts
index ff673402a..8f50a9d00 100644
--- a/src/api/endpoints/drive/folders/update.ts
+++ b/src/api/endpoints/drive/folders/update.ts
@@ -4,7 +4,7 @@
 import $ from 'cafy';
 import DriveFolder from '../../../models/drive-folder';
 import { isValidFolderName } from '../../../models/drive-folder';
-import serialize from '../../../serializers/drive-folder';
+import { pack } from '../../../models/drive-folder';
 import { publishDriveStream } from '../../../event';
 
 /**
@@ -91,7 +91,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 	});
 
 	// Serialize
-	const folderObj = await serialize(folder);
+	const folderObj = await pack(folder);
 
 	// Response
 	res(folderObj);
diff --git a/src/api/endpoints/drive/stream.ts b/src/api/endpoints/drive/stream.ts
index 5b0eb0a0d..3527d7050 100644
--- a/src/api/endpoints/drive/stream.ts
+++ b/src/api/endpoints/drive/stream.ts
@@ -3,7 +3,7 @@
  */
 import $ from 'cafy';
 import DriveFile from '../../models/drive-file';
-import serialize from '../../serializers/drive-file';
+import { pack } from '../../models/drive-file';
 
 /**
  * Get drive stream
@@ -64,5 +64,5 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 
 	// Serialize
 	res(await Promise.all(files.map(async file =>
-		await serialize(file))));
+		await pack(file))));
 });
diff --git a/src/api/endpoints/i.ts b/src/api/endpoints/i.ts
index ae75f11d5..1b6c1e58d 100644
--- a/src/api/endpoints/i.ts
+++ b/src/api/endpoints/i.ts
@@ -2,7 +2,7 @@
  * Module dependencies
  */
 import User from '../models/user';
-import serialize from '../serializers/user';
+import { pack } from '../models/user';
 
 /**
  * Show myself
@@ -15,7 +15,7 @@ import serialize from '../serializers/user';
  */
 module.exports = (params, user, _, isSecure) => new Promise(async (res, rej) => {
 	// Serialize
-	res(await serialize(user, user, {
+	res(await pack(user, user, {
 		detail: true,
 		includeSecrets: isSecure
 	}));
diff --git a/src/api/endpoints/i/authorized_apps.ts b/src/api/endpoints/i/authorized_apps.ts
index 807ca5b1e..40ce7a68c 100644
--- a/src/api/endpoints/i/authorized_apps.ts
+++ b/src/api/endpoints/i/authorized_apps.ts
@@ -3,7 +3,7 @@
  */
 import $ from 'cafy';
 import AccessToken from '../../models/access-token';
-import serialize from '../../serializers/app';
+import { pack } from '../../models/app';
 
 /**
  * Get authorized apps of my account
@@ -39,5 +39,5 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 
 	// Serialize
 	res(await Promise.all(tokens.map(async token =>
-		await serialize(token.app_id))));
+		await pack(token.app_id))));
 });
diff --git a/src/api/endpoints/i/favorites.ts b/src/api/endpoints/i/favorites.ts
index a66eaa754..eb464cf0f 100644
--- a/src/api/endpoints/i/favorites.ts
+++ b/src/api/endpoints/i/favorites.ts
@@ -3,7 +3,7 @@
  */
 import $ from 'cafy';
 import Favorite from '../../models/favorite';
-import serialize from '../../serializers/post';
+import { pack } from '../../models/post';
 
 /**
  * Get followers of a user
@@ -39,6 +39,6 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 
 	// Serialize
 	res(await Promise.all(favorites.map(async favorite =>
-		await serialize(favorite.post)
+		await pack(favorite.post)
 	)));
 });
diff --git a/src/api/endpoints/i/notifications.ts b/src/api/endpoints/i/notifications.ts
index fb9be7f61..688039a0d 100644
--- a/src/api/endpoints/i/notifications.ts
+++ b/src/api/endpoints/i/notifications.ts
@@ -4,7 +4,7 @@
 import $ from 'cafy';
 import Notification from '../../models/notification';
 import Mute from '../../models/mute';
-import serialize from '../../serializers/notification';
+import { pack } from '../../models/notification';
 import getFriends from '../../common/get-friends';
 import read from '../../common/read-notification';
 
@@ -101,7 +101,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 
 	// Serialize
 	res(await Promise.all(notifications.map(async notification =>
-		await serialize(notification))));
+		await pack(notification))));
 
 	// Mark as read all
 	if (notifications.length > 0 && markAsRead) {
diff --git a/src/api/endpoints/i/pin.ts b/src/api/endpoints/i/pin.ts
index a94950d22..ff546fc2b 100644
--- a/src/api/endpoints/i/pin.ts
+++ b/src/api/endpoints/i/pin.ts
@@ -4,7 +4,7 @@
 import $ from 'cafy';
 import User from '../../models/user';
 import Post from '../../models/post';
-import serialize from '../../serializers/user';
+import { pack } from '../../models/user';
 
 /**
  * Pin post
@@ -35,7 +35,7 @@ module.exports = async (params, user) => new Promise(async (res, rej) => {
 	});
 
 	// Serialize
-	const iObj = await serialize(user, user, {
+	const iObj = await pack(user, user, {
 		detail: true
 	});
 
diff --git a/src/api/endpoints/i/signin_history.ts b/src/api/endpoints/i/signin_history.ts
index e38bfa4d9..3ab59b694 100644
--- a/src/api/endpoints/i/signin_history.ts
+++ b/src/api/endpoints/i/signin_history.ts
@@ -3,7 +3,7 @@
  */
 import $ from 'cafy';
 import Signin from '../../models/signin';
-import serialize from '../../serializers/signin';
+import { pack } from '../../models/signin';
 
 /**
  * Get signin history of my account
@@ -58,5 +58,5 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 
 	// Serialize
 	res(await Promise.all(history.map(async record =>
-		await serialize(record))));
+		await pack(record))));
 });
diff --git a/src/api/endpoints/i/update.ts b/src/api/endpoints/i/update.ts
index c484c51a9..a138832e5 100644
--- a/src/api/endpoints/i/update.ts
+++ b/src/api/endpoints/i/update.ts
@@ -4,7 +4,7 @@
 import $ from 'cafy';
 import User from '../../models/user';
 import { isValidName, isValidDescription, isValidLocation, isValidBirthday } from '../../models/user';
-import serialize from '../../serializers/user';
+import { pack } from '../../models/user';
 import event from '../../event';
 import config from '../../../conf';
 
@@ -65,7 +65,7 @@ module.exports = async (params, user, _, isSecure) => new Promise(async (res, re
 	});
 
 	// Serialize
-	const iObj = await serialize(user, user, {
+	const iObj = await pack(user, user, {
 		detail: true,
 		includeSecrets: isSecure
 	});
diff --git a/src/api/endpoints/messaging/history.ts b/src/api/endpoints/messaging/history.ts
index f14740dff..1683ca7a8 100644
--- a/src/api/endpoints/messaging/history.ts
+++ b/src/api/endpoints/messaging/history.ts
@@ -4,7 +4,7 @@
 import $ from 'cafy';
 import History from '../../models/messaging-history';
 import Mute from '../../models/mute';
-import serialize from '../../serializers/messaging-message';
+import { pack } from '../../models/messaging-message';
 
 /**
  * Show messaging history
@@ -39,5 +39,5 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 
 	// Serialize
 	res(await Promise.all(history.map(async h =>
-		await serialize(h.message, user))));
+		await pack(h.message, user))));
 });
diff --git a/src/api/endpoints/messaging/messages.ts b/src/api/endpoints/messaging/messages.ts
index 3d3c6950a..67ba5e9d6 100644
--- a/src/api/endpoints/messaging/messages.ts
+++ b/src/api/endpoints/messaging/messages.ts
@@ -4,7 +4,7 @@
 import $ from 'cafy';
 import Message from '../../models/messaging-message';
 import User from '../../models/user';
-import serialize from '../../serializers/messaging-message';
+import { pack } from '../../models/messaging-message';
 import read from '../../common/read-messaging-message';
 
 /**
@@ -87,7 +87,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 
 	// Serialize
 	res(await Promise.all(messages.map(async message =>
-		await serialize(message, user, {
+		await pack(message, user, {
 			populateRecipient: false
 		}))));
 
diff --git a/src/api/endpoints/messaging/messages/create.ts b/src/api/endpoints/messaging/messages/create.ts
index 4e9d10197..1b8a5f59e 100644
--- a/src/api/endpoints/messaging/messages/create.ts
+++ b/src/api/endpoints/messaging/messages/create.ts
@@ -8,7 +8,7 @@ import History from '../../../models/messaging-history';
 import User from '../../../models/user';
 import Mute from '../../../models/mute';
 import DriveFile from '../../../models/drive-file';
-import serialize from '../../../serializers/messaging-message';
+import { pack } from '../../../models/messaging-message';
 import publishUserStream from '../../../event';
 import { publishMessagingStream, publishMessagingIndexStream, pushSw } from '../../../event';
 import config from '../../../../conf';
@@ -79,7 +79,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 	});
 
 	// Serialize
-	const messageObj = await serialize(message);
+	const messageObj = await pack(message);
 
 	// Reponse
 	res(messageObj);
diff --git a/src/api/endpoints/mute/list.ts b/src/api/endpoints/mute/list.ts
index 740e19f0b..19e3b157e 100644
--- a/src/api/endpoints/mute/list.ts
+++ b/src/api/endpoints/mute/list.ts
@@ -3,7 +3,7 @@
  */
 import $ from 'cafy';
 import Mute from '../../models/mute';
-import serialize from '../../serializers/user';
+import { pack } from '../../models/user';
 import getFriends from '../../common/get-friends';
 
 /**
@@ -63,7 +63,7 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
 
 	// Serialize
 	const users = await Promise.all(mutes.map(async m =>
-		await serialize(m.mutee_id, me, { detail: true })));
+		await pack(m.mutee_id, me, { detail: true })));
 
 	// Response
 	res({
diff --git a/src/api/endpoints/my/apps.ts b/src/api/endpoints/my/apps.ts
index eb9c75876..fe583db86 100644
--- a/src/api/endpoints/my/apps.ts
+++ b/src/api/endpoints/my/apps.ts
@@ -3,7 +3,7 @@
  */
 import $ from 'cafy';
 import App from '../../models/app';
-import serialize from '../../serializers/app';
+import { pack } from '../../models/app';
 
 /**
  * Get my apps
@@ -37,5 +37,5 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 
 	// Reply
 	res(await Promise.all(apps.map(async app =>
-		await serialize(app))));
+		await pack(app))));
 });
diff --git a/src/api/endpoints/posts.ts b/src/api/endpoints/posts.ts
index db166cd67..d10c6ab40 100644
--- a/src/api/endpoints/posts.ts
+++ b/src/api/endpoints/posts.ts
@@ -3,7 +3,7 @@
  */
 import $ from 'cafy';
 import Post from '../models/post';
-import serialize from '../serializers/post';
+import { pack } from '../models/post';
 
 /**
  * Lists all posts
@@ -85,5 +85,5 @@ module.exports = (params) => new Promise(async (res, rej) => {
 		});
 
 	// Serialize
-	res(await Promise.all(posts.map(async post => await serialize(post))));
+	res(await Promise.all(posts.map(async post => await pack(post))));
 });
diff --git a/src/api/endpoints/posts/context.ts b/src/api/endpoints/posts/context.ts
index bad59a6be..3051e7af1 100644
--- a/src/api/endpoints/posts/context.ts
+++ b/src/api/endpoints/posts/context.ts
@@ -3,7 +3,7 @@
  */
 import $ from 'cafy';
 import Post from '../../models/post';
-import serialize from '../../serializers/post';
+import { pack } from '../../models/post';
 
 /**
  * Show a context of a post
@@ -60,5 +60,5 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 
 	// Serialize
 	res(await Promise.all(context.map(async post =>
-		await serialize(post, user))));
+		await pack(post, user))));
 });
diff --git a/src/api/endpoints/posts/create.ts b/src/api/endpoints/posts/create.ts
index a1d05c67c..0fa52221f 100644
--- a/src/api/endpoints/posts/create.ts
+++ b/src/api/endpoints/posts/create.ts
@@ -12,7 +12,7 @@ import Mute from '../../models/mute';
 import DriveFile from '../../models/drive-file';
 import Watching from '../../models/post-watching';
 import ChannelWatching from '../../models/channel-watching';
-import serialize from '../../serializers/post';
+import { pack } from '../../models/post';
 import notify from '../../common/notify';
 import watch from '../../common/watch-post';
 import event, { pushSw, publishChannelStream } from '../../event';
@@ -224,7 +224,7 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => {
 	});
 
 	// Serialize
-	const postObj = await serialize(post);
+	const postObj = await pack(post);
 
 	// Reponse
 	res({
diff --git a/src/api/endpoints/posts/mentions.ts b/src/api/endpoints/posts/mentions.ts
index 3bb4ec3fa..7127db0ad 100644
--- a/src/api/endpoints/posts/mentions.ts
+++ b/src/api/endpoints/posts/mentions.ts
@@ -4,7 +4,7 @@
 import $ from 'cafy';
 import Post from '../../models/post';
 import getFriends from '../../common/get-friends';
-import serialize from '../../serializers/post';
+import { pack } from '../../models/post';
 
 /**
  * Get mentions of myself
@@ -73,6 +73,6 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 
 	// Serialize
 	res(await Promise.all(mentions.map(async mention =>
-		await serialize(mention, user)
+		await pack(mention, user)
 	)));
 });
diff --git a/src/api/endpoints/posts/polls/recommendation.ts b/src/api/endpoints/posts/polls/recommendation.ts
index 9c92d6cac..5ccb75449 100644
--- a/src/api/endpoints/posts/polls/recommendation.ts
+++ b/src/api/endpoints/posts/polls/recommendation.ts
@@ -4,7 +4,7 @@
 import $ from 'cafy';
 import Vote from '../../../models/poll-vote';
 import Post from '../../../models/post';
-import serialize from '../../../serializers/post';
+import { pack } from '../../../models/post';
 
 /**
  * Get recommended polls
@@ -56,5 +56,5 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 
 	// Serialize
 	res(await Promise.all(posts.map(async post =>
-		await serialize(post, user, { detail: true }))));
+		await pack(post, user, { detail: true }))));
 });
diff --git a/src/api/endpoints/posts/reactions.ts b/src/api/endpoints/posts/reactions.ts
index eab5d9b25..f60334df8 100644
--- a/src/api/endpoints/posts/reactions.ts
+++ b/src/api/endpoints/posts/reactions.ts
@@ -4,7 +4,7 @@
 import $ from 'cafy';
 import Post from '../../models/post';
 import Reaction from '../../models/post-reaction';
-import serialize from '../../serializers/post-reaction';
+import { pack } from '../../models/post-reaction';
 
 /**
  * Show reactions of a post
@@ -54,5 +54,5 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 
 	// Serialize
 	res(await Promise.all(reactions.map(async reaction =>
-		await serialize(reaction, user))));
+		await pack(reaction, user))));
 });
diff --git a/src/api/endpoints/posts/replies.ts b/src/api/endpoints/posts/replies.ts
index 3fd6a4676..1442b8a4c 100644
--- a/src/api/endpoints/posts/replies.ts
+++ b/src/api/endpoints/posts/replies.ts
@@ -3,7 +3,7 @@
  */
 import $ from 'cafy';
 import Post from '../../models/post';
-import serialize from '../../serializers/post';
+import { pack } from '../../models/post';
 
 /**
  * Show a replies of a post
@@ -50,5 +50,5 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 
 	// Serialize
 	res(await Promise.all(replies.map(async post =>
-		await serialize(post, user))));
+		await pack(post, user))));
 });
diff --git a/src/api/endpoints/posts/reposts.ts b/src/api/endpoints/posts/reposts.ts
index bcc6163a1..0fbb0687b 100644
--- a/src/api/endpoints/posts/reposts.ts
+++ b/src/api/endpoints/posts/reposts.ts
@@ -3,7 +3,7 @@
  */
 import $ from 'cafy';
 import Post from '../../models/post';
-import serialize from '../../serializers/post';
+import { pack } from '../../models/post';
 
 /**
  * Show a reposts of a post
@@ -70,5 +70,5 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 
 	// Serialize
 	res(await Promise.all(reposts.map(async post =>
-		await serialize(post, user))));
+		await pack(post, user))));
 });
diff --git a/src/api/endpoints/posts/search.ts b/src/api/endpoints/posts/search.ts
index 31c9a8d3c..6e26f5539 100644
--- a/src/api/endpoints/posts/search.ts
+++ b/src/api/endpoints/posts/search.ts
@@ -7,7 +7,7 @@ import Post from '../../models/post';
 import User from '../../models/user';
 import Mute from '../../models/mute';
 import getFriends from '../../common/get-friends';
-import serialize from '../../serializers/post';
+import { pack } from '../../models/post';
 
 /**
  * Search a post
@@ -351,5 +351,5 @@ async function search(
 
 	// Serialize
 	res(await Promise.all(posts.map(async post =>
-		await serialize(post, me))));
+		await pack(post, me))));
 }
diff --git a/src/api/endpoints/posts/show.ts b/src/api/endpoints/posts/show.ts
index 5bfe4f660..c31244971 100644
--- a/src/api/endpoints/posts/show.ts
+++ b/src/api/endpoints/posts/show.ts
@@ -3,7 +3,7 @@
  */
 import $ from 'cafy';
 import Post from '../../models/post';
-import serialize from '../../serializers/post';
+import { pack } from '../../models/post';
 
 /**
  * Show a post
@@ -27,7 +27,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 	}
 
 	// Serialize
-	res(await serialize(post, user, {
+	res(await pack(post, user, {
 		detail: true
 	}));
 });
diff --git a/src/api/endpoints/posts/timeline.ts b/src/api/endpoints/posts/timeline.ts
index da7ffd0c1..c41cfdb8b 100644
--- a/src/api/endpoints/posts/timeline.ts
+++ b/src/api/endpoints/posts/timeline.ts
@@ -7,7 +7,7 @@ import Post from '../../models/post';
 import Mute from '../../models/mute';
 import ChannelWatching from '../../models/channel-watching';
 import getFriends from '../../common/get-friends';
-import serialize from '../../serializers/post';
+import { pack } from '../../models/post';
 
 /**
  * Get timeline of myself
@@ -128,5 +128,5 @@ module.exports = async (params, user, app) => {
 		});
 
 	// Serialize
-	return await Promise.all(timeline.map(post => serialize(post, user)));
+	return await Promise.all(timeline.map(post => pack(post, user)));
 };
diff --git a/src/api/endpoints/posts/trend.ts b/src/api/endpoints/posts/trend.ts
index 64a195dff..b2b1d327a 100644
--- a/src/api/endpoints/posts/trend.ts
+++ b/src/api/endpoints/posts/trend.ts
@@ -4,7 +4,7 @@
 const ms = require('ms');
 import $ from 'cafy';
 import Post from '../../models/post';
-import serialize from '../../serializers/post';
+import { pack } from '../../models/post';
 
 /**
  * Get trend posts
@@ -76,5 +76,5 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 
 	// Serialize
 	res(await Promise.all(posts.map(async post =>
-		await serialize(post, user, { detail: true }))));
+		await pack(post, user, { detail: true }))));
 });
diff --git a/src/api/endpoints/users.ts b/src/api/endpoints/users.ts
index f3c9b66a5..ba33b1aeb 100644
--- a/src/api/endpoints/users.ts
+++ b/src/api/endpoints/users.ts
@@ -3,7 +3,7 @@
  */
 import $ from 'cafy';
 import User from '../models/user';
-import serialize from '../serializers/user';
+import { pack } from '../models/user';
 
 /**
  * Lists all users
@@ -55,5 +55,5 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
 
 	// Serialize
 	res(await Promise.all(users.map(async user =>
-		await serialize(user, me))));
+		await pack(user, me))));
 });
diff --git a/src/api/endpoints/users/followers.ts b/src/api/endpoints/users/followers.ts
index 4905323ba..b0fb83c68 100644
--- a/src/api/endpoints/users/followers.ts
+++ b/src/api/endpoints/users/followers.ts
@@ -4,7 +4,7 @@
 import $ from 'cafy';
 import User from '../../models/user';
 import Following from '../../models/following';
-import serialize from '../../serializers/user';
+import { pack } from '../../models/user';
 import getFriends from '../../common/get-friends';
 
 /**
@@ -82,7 +82,7 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
 
 	// Serialize
 	const users = await Promise.all(following.map(async f =>
-		await serialize(f.follower_id, me, { detail: true })));
+		await pack(f.follower_id, me, { detail: true })));
 
 	// Response
 	res({
diff --git a/src/api/endpoints/users/following.ts b/src/api/endpoints/users/following.ts
index dc2ff49bb..8e88431e9 100644
--- a/src/api/endpoints/users/following.ts
+++ b/src/api/endpoints/users/following.ts
@@ -4,7 +4,7 @@
 import $ from 'cafy';
 import User from '../../models/user';
 import Following from '../../models/following';
-import serialize from '../../serializers/user';
+import { pack } from '../../models/user';
 import getFriends from '../../common/get-friends';
 
 /**
@@ -82,7 +82,7 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
 
 	// Serialize
 	const users = await Promise.all(following.map(async f =>
-		await serialize(f.followee_id, me, { detail: true })));
+		await pack(f.followee_id, me, { detail: true })));
 
 	// Response
 	res({
diff --git a/src/api/endpoints/users/get_frequently_replied_users.ts b/src/api/endpoints/users/get_frequently_replied_users.ts
index a8add623d..3cbc76132 100644
--- a/src/api/endpoints/users/get_frequently_replied_users.ts
+++ b/src/api/endpoints/users/get_frequently_replied_users.ts
@@ -4,7 +4,7 @@
 import $ from 'cafy';
 import Post from '../../models/post';
 import User from '../../models/user';
-import serialize from '../../serializers/user';
+import { pack } from '../../models/user';
 
 module.exports = (params, me) => new Promise(async (res, rej) => {
 	// Get 'user_id' parameter
@@ -91,7 +91,7 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
 
 	// Make replies object (includes weights)
 	const repliesObj = await Promise.all(topRepliedUsers.map(async (user) => ({
-		user: await serialize(user, me, { detail: true }),
+		user: await pack(user, me, { detail: true }),
 		weight: repliedUsers[user] / peak
 	})));
 
diff --git a/src/api/endpoints/users/posts.ts b/src/api/endpoints/users/posts.ts
index 0d8384a43..1f3db3cf7 100644
--- a/src/api/endpoints/users/posts.ts
+++ b/src/api/endpoints/users/posts.ts
@@ -4,7 +4,7 @@
 import $ from 'cafy';
 import Post from '../../models/post';
 import User from '../../models/user';
-import serialize from '../../serializers/post';
+import { pack } from '../../models/post';
 
 /**
  * Get posts of a user
@@ -124,6 +124,6 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
 
 	// Serialize
 	res(await Promise.all(posts.map(async (post) =>
-		await serialize(post, me)
+		await pack(post, me)
 	)));
 });
diff --git a/src/api/endpoints/users/recommendation.ts b/src/api/endpoints/users/recommendation.ts
index 731d68a7b..b80fd63ce 100644
--- a/src/api/endpoints/users/recommendation.ts
+++ b/src/api/endpoints/users/recommendation.ts
@@ -4,7 +4,7 @@
 const ms = require('ms');
 import $ from 'cafy';
 import User from '../../models/user';
-import serialize from '../../serializers/user';
+import { pack } from '../../models/user';
 import getFriends from '../../common/get-friends';
 
 /**
@@ -44,5 +44,5 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
 
 	// Serialize
 	res(await Promise.all(users.map(async user =>
-		await serialize(user, me, { detail: true }))));
+		await pack(user, me, { detail: true }))));
 });
diff --git a/src/api/endpoints/users/search.ts b/src/api/endpoints/users/search.ts
index 73a5db47e..213038403 100644
--- a/src/api/endpoints/users/search.ts
+++ b/src/api/endpoints/users/search.ts
@@ -4,7 +4,7 @@
 import * as mongo from 'mongodb';
 import $ from 'cafy';
 import User from '../../models/user';
-import serialize from '../../serializers/user';
+import { pack } from '../../models/user';
 import config from '../../../conf';
 const escapeRegexp = require('escape-regexp');
 
@@ -94,6 +94,6 @@ async function byElasticsearch(res, rej, me, query, offset, max) {
 
 		// Serialize
 		res(await Promise.all(users.map(async user =>
-			await serialize(user, me, { detail: true }))));
+			await pack(user, me, { detail: true }))));
 	});
 }
diff --git a/src/api/endpoints/users/search_by_username.ts b/src/api/endpoints/users/search_by_username.ts
index 7f2f42f0a..63e206b1f 100644
--- a/src/api/endpoints/users/search_by_username.ts
+++ b/src/api/endpoints/users/search_by_username.ts
@@ -3,7 +3,7 @@
  */
 import $ from 'cafy';
 import User from '../../models/user';
-import serialize from '../../serializers/user';
+import { pack } from '../../models/user';
 
 /**
  * Search a user by username
@@ -35,5 +35,5 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
 
 	// Serialize
 	res(await Promise.all(users.map(async user =>
-		await serialize(user, me, { detail: true }))));
+		await pack(user, me, { detail: true }))));
 });
diff --git a/src/api/endpoints/users/show.ts b/src/api/endpoints/users/show.ts
index 8e74b0fe3..a51cb619d 100644
--- a/src/api/endpoints/users/show.ts
+++ b/src/api/endpoints/users/show.ts
@@ -3,7 +3,7 @@
  */
 import $ from 'cafy';
 import User from '../../models/user';
-import serialize from '../../serializers/user';
+import { pack } from '../../models/user';
 
 /**
  * Show a user
@@ -41,7 +41,7 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
 	}
 
 	// Send response
-	res(await serialize(user, me, {
+	res(await pack(user, me, {
 		detail: true
 	}));
 });
diff --git a/src/api/models/drive-file.ts b/src/api/models/drive-file.ts
index 6a8db3ad4..9b9df1dac 100644
--- a/src/api/models/drive-file.ts
+++ b/src/api/models/drive-file.ts
@@ -20,8 +20,14 @@ export { getGridFSBucket };
 
 export type IDriveFile = {
 	_id: mongodb.ObjectID;
-	created_at: Date;
-	user_id: mongodb.ObjectID;
+	uploadDate: Date;
+	md5: string;
+	filename: string;
+	metadata: {
+		properties: any;
+		user_id: mongodb.ObjectID;
+		folder_id: mongodb.ObjectID;
+	}
 };
 
 export function validateFileName(name: string): boolean {
diff --git a/src/api/models/drive-folder.ts b/src/api/models/drive-folder.ts
index 48b26c2bd..54b45049b 100644
--- a/src/api/models/drive-folder.ts
+++ b/src/api/models/drive-folder.ts
@@ -9,7 +9,9 @@ export default DriveFolder;
 export type IDriveFolder = {
 	_id: mongo.ObjectID;
 	created_at: Date;
+	name: string;
 	user_id: mongo.ObjectID;
+	parent_id: mongo.ObjectID;
 };
 
 export function isValidFolderName(name: string): boolean {
diff --git a/src/api/models/messaging-message.ts b/src/api/models/messaging-message.ts
index ffdda1db2..90cf1cd71 100644
--- a/src/api/models/messaging-message.ts
+++ b/src/api/models/messaging-message.ts
@@ -10,6 +10,11 @@ export default MessagingMessage;
 
 export interface IMessagingMessage {
 	_id: mongo.ObjectID;
+	created_at: Date;
+	text: string;
+	user_id: mongo.ObjectID;
+	recipient_id: mongo.ObjectID;
+	is_read: boolean;
 }
 
 export function isValidText(text: string): boolean {
diff --git a/src/api/private/signin.ts b/src/api/private/signin.ts
index a26c8f6c5..ab6e93562 100644
--- a/src/api/private/signin.ts
+++ b/src/api/private/signin.ts
@@ -3,7 +3,7 @@ import * as bcrypt from 'bcryptjs';
 import * as speakeasy from 'speakeasy';
 import { default as User, IUser } from '../models/user';
 import Signin from '../models/signin';
-import serialize from '../serializers/signin';
+import { pack } from '../models/signin';
 import event from '../event';
 import signin from '../common/signin';
 import config from '../../conf';
@@ -85,5 +85,5 @@ export default async (req: express.Request, res: express.Response) => {
 	});
 
 	// Publish signin event
-	event(user._id, 'signin', await serialize(record));
+	event(user._id, 'signin', await pack(record));
 };
diff --git a/src/api/private/signup.ts b/src/api/private/signup.ts
index 466c6a489..105fe319a 100644
--- a/src/api/private/signup.ts
+++ b/src/api/private/signup.ts
@@ -4,7 +4,7 @@ import * as bcrypt from 'bcryptjs';
 import recaptcha = require('recaptcha-promise');
 import { default as User, IUser } from '../models/user';
 import { validateUsername, validatePassword } from '../models/user';
-import serialize from '../serializers/user';
+import { pack } from '../models/user';
 import generateUserToken from '../common/generate-native-user-token';
 import config from '../../conf';
 
@@ -142,7 +142,7 @@ export default async (req: express.Request, res: express.Response) => {
 	});
 
 	// Response
-	res.send(await serialize(account));
+	res.send(await pack(account));
 
 	// Create search index
 	if (config.elasticsearch.enable) {
diff --git a/src/api/service/twitter.ts b/src/api/service/twitter.ts
index 0e75ee0bd..ca4f8abcc 100644
--- a/src/api/service/twitter.ts
+++ b/src/api/service/twitter.ts
@@ -6,7 +6,7 @@ import * as uuid from 'uuid';
 import autwh from 'autwh';
 import redis from '../../db/redis';
 import User from '../models/user';
-import serialize from '../serializers/user';
+import { pack } from '../models/user';
 import event from '../event';
 import config from '../../conf';
 import signin from '../common/signin';
@@ -50,7 +50,7 @@ module.exports = (app: express.Application) => {
 		res.send(`Twitterの連携を解除しました :v:`);
 
 		// Publish i updated event
-		event(user._id, 'i_updated', await serialize(user, user, {
+		event(user._id, 'i_updated', await pack(user, user, {
 			detail: true,
 			includeSecrets: true
 		}));

From 2cb0511dba7463ad50725fd2dfd1966f0a108a45 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=93=E3=81=B4=E3=81=AA=E3=81=9F=E3=81=BF=E3=81=BD?=
 <syuilotan@yahoo.co.jp>
Date: Fri, 2 Feb 2018 10:31:17 +0900
Subject: [PATCH 185/186] wip

---
 src/api/endpoints/app/create.ts                         | 3 +--
 src/api/endpoints/app/show.ts                           | 3 +--
 src/api/endpoints/auth/session/show.ts                  | 3 +--
 src/api/endpoints/channels.ts                           | 3 +--
 src/api/endpoints/channels/posts.ts                     | 3 +--
 src/api/endpoints/channels/show.ts                      | 3 +--
 src/api/endpoints/drive/files.ts                        | 3 +--
 src/api/endpoints/drive/files/create.ts                 | 3 +--
 src/api/endpoints/drive/files/find.ts                   | 3 +--
 src/api/endpoints/drive/files/show.ts                   | 3 +--
 src/api/endpoints/drive/files/update.ts                 | 3 +--
 src/api/endpoints/drive/files/upload_from_url.ts        | 3 +--
 src/api/endpoints/drive/folders.ts                      | 3 +--
 src/api/endpoints/drive/folders/create.ts               | 3 +--
 src/api/endpoints/drive/folders/find.ts                 | 3 +--
 src/api/endpoints/drive/folders/show.ts                 | 3 +--
 src/api/endpoints/drive/folders/update.ts               | 3 +--
 src/api/endpoints/drive/stream.ts                       | 3 +--
 src/api/endpoints/i.ts                                  | 3 +--
 src/api/endpoints/i/signin_history.ts                   | 3 +--
 src/api/endpoints/i/update.ts                           | 3 +--
 src/api/endpoints/my/apps.ts                            | 3 +--
 src/api/endpoints/posts.ts                              | 3 +--
 src/api/endpoints/posts/context.ts                      | 3 +--
 src/api/endpoints/posts/polls/recommendation.ts         | 3 +--
 src/api/endpoints/posts/reactions.ts                    | 3 +--
 src/api/endpoints/posts/replies.ts                      | 3 +--
 src/api/endpoints/posts/reposts.ts                      | 3 +--
 src/api/endpoints/posts/show.ts                         | 3 +--
 src/api/endpoints/posts/trend.ts                        | 3 +--
 src/api/endpoints/users.ts                              | 3 +--
 src/api/endpoints/users/get_frequently_replied_users.ts | 3 +--
 src/api/endpoints/users/posts.ts                        | 3 +--
 src/api/endpoints/users/recommendation.ts               | 3 +--
 src/api/endpoints/users/search.ts                       | 3 +--
 src/api/endpoints/users/search_by_username.ts           | 3 +--
 src/api/endpoints/users/show.ts                         | 3 +--
 src/api/private/signin.ts                               | 3 +--
 src/api/private/signup.ts                               | 3 +--
 src/api/service/twitter.ts                              | 3 +--
 40 files changed, 40 insertions(+), 80 deletions(-)

diff --git a/src/api/endpoints/app/create.ts b/src/api/endpoints/app/create.ts
index 320163ebd..71633f7de 100644
--- a/src/api/endpoints/app/create.ts
+++ b/src/api/endpoints/app/create.ts
@@ -4,8 +4,7 @@
 import rndstr from 'rndstr';
 import $ from 'cafy';
 import App from '../../models/app';
-import { isValidNameId } from '../../models/app';
-import { pack } from '../../models/app';
+import { isValidNameId }, { pack } from '../../models/app';
 
 /**
  * @swagger
diff --git a/src/api/endpoints/app/show.ts b/src/api/endpoints/app/show.ts
index a3ef24717..8bc3dda42 100644
--- a/src/api/endpoints/app/show.ts
+++ b/src/api/endpoints/app/show.ts
@@ -2,8 +2,7 @@
  * Module dependencies
  */
 import $ from 'cafy';
-import App from '../../models/app';
-import { pack } from '../../models/app';
+import App, { pack } from '../../models/app';
 
 /**
  * @swagger
diff --git a/src/api/endpoints/auth/session/show.ts b/src/api/endpoints/auth/session/show.ts
index 1fe3b873f..73ac3185f 100644
--- a/src/api/endpoints/auth/session/show.ts
+++ b/src/api/endpoints/auth/session/show.ts
@@ -2,8 +2,7 @@
  * Module dependencies
  */
 import $ from 'cafy';
-import AuthSess from '../../../models/auth-session';
-import { pack } from '../../../models/auth-session';
+import AuthSess, { pack } from '../../../models/auth-session';
 
 /**
  * @swagger
diff --git a/src/api/endpoints/channels.ts b/src/api/endpoints/channels.ts
index 92dcee83d..b9a7d1b78 100644
--- a/src/api/endpoints/channels.ts
+++ b/src/api/endpoints/channels.ts
@@ -2,8 +2,7 @@
  * Module dependencies
  */
 import $ from 'cafy';
-import Channel from '../models/channel';
-import { pack } from '../models/channel';
+import Channel, { pack } from '../models/channel';
 
 /**
  * Get all channels
diff --git a/src/api/endpoints/channels/posts.ts b/src/api/endpoints/channels/posts.ts
index 3feee51f7..d722589c2 100644
--- a/src/api/endpoints/channels/posts.ts
+++ b/src/api/endpoints/channels/posts.ts
@@ -3,8 +3,7 @@
  */
 import $ from 'cafy';
 import { default as Channel, IChannel } from '../../models/channel';
-import Post from '../../models/post';
-import { pack } from '../../models/post';
+import Post, { pack } from '../../models/post';
 
 /**
  * Show a posts of a channel
diff --git a/src/api/endpoints/channels/show.ts b/src/api/endpoints/channels/show.ts
index 89c48379a..3238616fa 100644
--- a/src/api/endpoints/channels/show.ts
+++ b/src/api/endpoints/channels/show.ts
@@ -2,8 +2,7 @@
  * Module dependencies
  */
 import $ from 'cafy';
-import { default as Channel, IChannel } from '../../models/channel';
-import { pack } from '../../models/channel';
+import { default as Channel, IChannel }, { pack } from '../../models/channel';
 
 /**
  * Show a channel
diff --git a/src/api/endpoints/drive/files.ts b/src/api/endpoints/drive/files.ts
index 3bd80e728..89915331e 100644
--- a/src/api/endpoints/drive/files.ts
+++ b/src/api/endpoints/drive/files.ts
@@ -2,8 +2,7 @@
  * Module dependencies
  */
 import $ from 'cafy';
-import DriveFile from '../../models/drive-file';
-import { pack } from '../../models/drive-file';
+import DriveFile, { pack } from '../../models/drive-file';
 
 /**
  * Get drive files
diff --git a/src/api/endpoints/drive/files/create.ts b/src/api/endpoints/drive/files/create.ts
index 6fa76d7e9..7b424f3f5 100644
--- a/src/api/endpoints/drive/files/create.ts
+++ b/src/api/endpoints/drive/files/create.ts
@@ -2,8 +2,7 @@
  * Module dependencies
  */
 import $ from 'cafy';
-import { validateFileName } from '../../../models/drive-file';
-import { pack } from '../../../models/drive-file';
+import { validateFileName }, { pack } from '../../../models/drive-file';
 import create from '../../../common/add-file-to-drive';
 
 /**
diff --git a/src/api/endpoints/drive/files/find.ts b/src/api/endpoints/drive/files/find.ts
index 571aba81f..e026afe93 100644
--- a/src/api/endpoints/drive/files/find.ts
+++ b/src/api/endpoints/drive/files/find.ts
@@ -2,8 +2,7 @@
  * Module dependencies
  */
 import $ from 'cafy';
-import DriveFile from '../../../models/drive-file';
-import { pack } from '../../../models/drive-file';
+import DriveFile, { pack } from '../../../models/drive-file';
 
 /**
  * Find a file(s)
diff --git a/src/api/endpoints/drive/files/show.ts b/src/api/endpoints/drive/files/show.ts
index 00f69f141..21664f7ba 100644
--- a/src/api/endpoints/drive/files/show.ts
+++ b/src/api/endpoints/drive/files/show.ts
@@ -2,8 +2,7 @@
  * Module dependencies
  */
 import $ from 'cafy';
-import DriveFile from '../../../models/drive-file';
-import { pack } from '../../../models/drive-file';
+import DriveFile, { pack } from '../../../models/drive-file';
 
 /**
  * Show a file
diff --git a/src/api/endpoints/drive/files/update.ts b/src/api/endpoints/drive/files/update.ts
index 9ef8215b1..ff65a48f7 100644
--- a/src/api/endpoints/drive/files/update.ts
+++ b/src/api/endpoints/drive/files/update.ts
@@ -4,8 +4,7 @@
 import $ from 'cafy';
 import DriveFolder from '../../../models/drive-folder';
 import DriveFile from '../../../models/drive-file';
-import { validateFileName } from '../../../models/drive-file';
-import { pack } from '../../../models/drive-file';
+import { validateFileName }, { pack } from '../../../models/drive-file';
 import { publishDriveStream } from '../../../event';
 
 /**
diff --git a/src/api/endpoints/drive/files/upload_from_url.ts b/src/api/endpoints/drive/files/upload_from_url.ts
index f0398bfc5..009f06aaa 100644
--- a/src/api/endpoints/drive/files/upload_from_url.ts
+++ b/src/api/endpoints/drive/files/upload_from_url.ts
@@ -3,8 +3,7 @@
  */
 import * as URL from 'url';
 import $ from 'cafy';
-import { validateFileName } from '../../../models/drive-file';
-import { pack } from '../../../models/drive-file';
+import { validateFileName }, { pack } from '../../../models/drive-file';
 import create from '../../../common/add-file-to-drive';
 import * as debug from 'debug';
 import * as tmp from 'tmp';
diff --git a/src/api/endpoints/drive/folders.ts b/src/api/endpoints/drive/folders.ts
index e650fb74a..428bde350 100644
--- a/src/api/endpoints/drive/folders.ts
+++ b/src/api/endpoints/drive/folders.ts
@@ -2,8 +2,7 @@
  * Module dependencies
  */
 import $ from 'cafy';
-import DriveFolder from '../../models/drive-folder';
-import { pack } from '../../models/drive-folder';
+import DriveFolder, { pack } from '../../models/drive-folder';
 
 /**
  * Get drive folders
diff --git a/src/api/endpoints/drive/folders/create.ts b/src/api/endpoints/drive/folders/create.ts
index 1953c09ee..6543b1127 100644
--- a/src/api/endpoints/drive/folders/create.ts
+++ b/src/api/endpoints/drive/folders/create.ts
@@ -3,8 +3,7 @@
  */
 import $ from 'cafy';
 import DriveFolder from '../../../models/drive-folder';
-import { isValidFolderName } from '../../../models/drive-folder';
-import { pack } from '../../../models/drive-folder';
+import { isValidFolderName }, { pack } from '../../../models/drive-folder';
 import { publishDriveStream } from '../../../event';
 
 /**
diff --git a/src/api/endpoints/drive/folders/find.ts b/src/api/endpoints/drive/folders/find.ts
index caad45d74..fc84766bc 100644
--- a/src/api/endpoints/drive/folders/find.ts
+++ b/src/api/endpoints/drive/folders/find.ts
@@ -2,8 +2,7 @@
  * Module dependencies
  */
 import $ from 'cafy';
-import DriveFolder from '../../../models/drive-folder';
-import { pack } from '../../../models/drive-folder';
+import DriveFolder, { pack } from '../../../models/drive-folder';
 
 /**
  * Find a folder(s)
diff --git a/src/api/endpoints/drive/folders/show.ts b/src/api/endpoints/drive/folders/show.ts
index fd3061ca5..e07d14d20 100644
--- a/src/api/endpoints/drive/folders/show.ts
+++ b/src/api/endpoints/drive/folders/show.ts
@@ -2,8 +2,7 @@
  * Module dependencies
  */
 import $ from 'cafy';
-import DriveFolder from '../../../models/drive-folder';
-import { pack } from '../../../models/drive-folder';
+import DriveFolder, { pack } from '../../../models/drive-folder';
 
 /**
  * Show a folder
diff --git a/src/api/endpoints/drive/folders/update.ts b/src/api/endpoints/drive/folders/update.ts
index 8f50a9d00..2adcadcb0 100644
--- a/src/api/endpoints/drive/folders/update.ts
+++ b/src/api/endpoints/drive/folders/update.ts
@@ -3,8 +3,7 @@
  */
 import $ from 'cafy';
 import DriveFolder from '../../../models/drive-folder';
-import { isValidFolderName } from '../../../models/drive-folder';
-import { pack } from '../../../models/drive-folder';
+import { isValidFolderName }, { pack } from '../../../models/drive-folder';
 import { publishDriveStream } from '../../../event';
 
 /**
diff --git a/src/api/endpoints/drive/stream.ts b/src/api/endpoints/drive/stream.ts
index 3527d7050..8352c7dd4 100644
--- a/src/api/endpoints/drive/stream.ts
+++ b/src/api/endpoints/drive/stream.ts
@@ -2,8 +2,7 @@
  * Module dependencies
  */
 import $ from 'cafy';
-import DriveFile from '../../models/drive-file';
-import { pack } from '../../models/drive-file';
+import DriveFile, { pack } from '../../models/drive-file';
 
 /**
  * Get drive stream
diff --git a/src/api/endpoints/i.ts b/src/api/endpoints/i.ts
index 1b6c1e58d..7efdbcd7c 100644
--- a/src/api/endpoints/i.ts
+++ b/src/api/endpoints/i.ts
@@ -1,8 +1,7 @@
 /**
  * Module dependencies
  */
-import User from '../models/user';
-import { pack } from '../models/user';
+import User, { pack } from '../models/user';
 
 /**
  * Show myself
diff --git a/src/api/endpoints/i/signin_history.ts b/src/api/endpoints/i/signin_history.ts
index 3ab59b694..859e81653 100644
--- a/src/api/endpoints/i/signin_history.ts
+++ b/src/api/endpoints/i/signin_history.ts
@@ -2,8 +2,7 @@
  * Module dependencies
  */
 import $ from 'cafy';
-import Signin from '../../models/signin';
-import { pack } from '../../models/signin';
+import Signin, { pack } from '../../models/signin';
 
 /**
  * Get signin history of my account
diff --git a/src/api/endpoints/i/update.ts b/src/api/endpoints/i/update.ts
index a138832e5..cd4b1a13f 100644
--- a/src/api/endpoints/i/update.ts
+++ b/src/api/endpoints/i/update.ts
@@ -3,8 +3,7 @@
  */
 import $ from 'cafy';
 import User from '../../models/user';
-import { isValidName, isValidDescription, isValidLocation, isValidBirthday } from '../../models/user';
-import { pack } from '../../models/user';
+import { isValidName, isValidDescription, isValidLocation, isValidBirthday }, { pack } from '../../models/user';
 import event from '../../event';
 import config from '../../../conf';
 
diff --git a/src/api/endpoints/my/apps.ts b/src/api/endpoints/my/apps.ts
index fe583db86..b23619050 100644
--- a/src/api/endpoints/my/apps.ts
+++ b/src/api/endpoints/my/apps.ts
@@ -2,8 +2,7 @@
  * Module dependencies
  */
 import $ from 'cafy';
-import App from '../../models/app';
-import { pack } from '../../models/app';
+import App, { pack } from '../../models/app';
 
 /**
  * Get my apps
diff --git a/src/api/endpoints/posts.ts b/src/api/endpoints/posts.ts
index d10c6ab40..3b2942592 100644
--- a/src/api/endpoints/posts.ts
+++ b/src/api/endpoints/posts.ts
@@ -2,8 +2,7 @@
  * Module dependencies
  */
 import $ from 'cafy';
-import Post from '../models/post';
-import { pack } from '../models/post';
+import Post, { pack } from '../models/post';
 
 /**
  * Lists all posts
diff --git a/src/api/endpoints/posts/context.ts b/src/api/endpoints/posts/context.ts
index 3051e7af1..5ba375897 100644
--- a/src/api/endpoints/posts/context.ts
+++ b/src/api/endpoints/posts/context.ts
@@ -2,8 +2,7 @@
  * Module dependencies
  */
 import $ from 'cafy';
-import Post from '../../models/post';
-import { pack } from '../../models/post';
+import Post, { pack } from '../../models/post';
 
 /**
  * Show a context of a post
diff --git a/src/api/endpoints/posts/polls/recommendation.ts b/src/api/endpoints/posts/polls/recommendation.ts
index 5ccb75449..4a3fa3f55 100644
--- a/src/api/endpoints/posts/polls/recommendation.ts
+++ b/src/api/endpoints/posts/polls/recommendation.ts
@@ -3,8 +3,7 @@
  */
 import $ from 'cafy';
 import Vote from '../../../models/poll-vote';
-import Post from '../../../models/post';
-import { pack } from '../../../models/post';
+import Post, { pack } from '../../../models/post';
 
 /**
  * Get recommended polls
diff --git a/src/api/endpoints/posts/reactions.ts b/src/api/endpoints/posts/reactions.ts
index f60334df8..feb140ab4 100644
--- a/src/api/endpoints/posts/reactions.ts
+++ b/src/api/endpoints/posts/reactions.ts
@@ -3,8 +3,7 @@
  */
 import $ from 'cafy';
 import Post from '../../models/post';
-import Reaction from '../../models/post-reaction';
-import { pack } from '../../models/post-reaction';
+import Reaction, { pack } from '../../models/post-reaction';
 
 /**
  * Show reactions of a post
diff --git a/src/api/endpoints/posts/replies.ts b/src/api/endpoints/posts/replies.ts
index 1442b8a4c..613c4fa24 100644
--- a/src/api/endpoints/posts/replies.ts
+++ b/src/api/endpoints/posts/replies.ts
@@ -2,8 +2,7 @@
  * Module dependencies
  */
 import $ from 'cafy';
-import Post from '../../models/post';
-import { pack } from '../../models/post';
+import Post, { pack } from '../../models/post';
 
 /**
  * Show a replies of a post
diff --git a/src/api/endpoints/posts/reposts.ts b/src/api/endpoints/posts/reposts.ts
index 0fbb0687b..89ab0e3d5 100644
--- a/src/api/endpoints/posts/reposts.ts
+++ b/src/api/endpoints/posts/reposts.ts
@@ -2,8 +2,7 @@
  * Module dependencies
  */
 import $ from 'cafy';
-import Post from '../../models/post';
-import { pack } from '../../models/post';
+import Post, { pack } from '../../models/post';
 
 /**
  * Show a reposts of a post
diff --git a/src/api/endpoints/posts/show.ts b/src/api/endpoints/posts/show.ts
index c31244971..383949059 100644
--- a/src/api/endpoints/posts/show.ts
+++ b/src/api/endpoints/posts/show.ts
@@ -2,8 +2,7 @@
  * Module dependencies
  */
 import $ from 'cafy';
-import Post from '../../models/post';
-import { pack } from '../../models/post';
+import Post, { pack } from '../../models/post';
 
 /**
  * Show a post
diff --git a/src/api/endpoints/posts/trend.ts b/src/api/endpoints/posts/trend.ts
index b2b1d327a..caded92bf 100644
--- a/src/api/endpoints/posts/trend.ts
+++ b/src/api/endpoints/posts/trend.ts
@@ -3,8 +3,7 @@
  */
 const ms = require('ms');
 import $ from 'cafy';
-import Post from '../../models/post';
-import { pack } from '../../models/post';
+import Post, { pack } from '../../models/post';
 
 /**
  * Get trend posts
diff --git a/src/api/endpoints/users.ts b/src/api/endpoints/users.ts
index ba33b1aeb..095b9fe40 100644
--- a/src/api/endpoints/users.ts
+++ b/src/api/endpoints/users.ts
@@ -2,8 +2,7 @@
  * Module dependencies
  */
 import $ from 'cafy';
-import User from '../models/user';
-import { pack } from '../models/user';
+import User, { pack } from '../models/user';
 
 /**
  * Lists all users
diff --git a/src/api/endpoints/users/get_frequently_replied_users.ts b/src/api/endpoints/users/get_frequently_replied_users.ts
index 3cbc76132..87f4f77a5 100644
--- a/src/api/endpoints/users/get_frequently_replied_users.ts
+++ b/src/api/endpoints/users/get_frequently_replied_users.ts
@@ -3,8 +3,7 @@
  */
 import $ from 'cafy';
 import Post from '../../models/post';
-import User from '../../models/user';
-import { pack } from '../../models/user';
+import User, { pack } from '../../models/user';
 
 module.exports = (params, me) => new Promise(async (res, rej) => {
 	// Get 'user_id' parameter
diff --git a/src/api/endpoints/users/posts.ts b/src/api/endpoints/users/posts.ts
index 1f3db3cf7..285e5bc46 100644
--- a/src/api/endpoints/users/posts.ts
+++ b/src/api/endpoints/users/posts.ts
@@ -3,8 +3,7 @@
  */
 import $ from 'cafy';
 import Post from '../../models/post';
-import User from '../../models/user';
-import { pack } from '../../models/post';
+import User, { pack } from '../../models/user';
 
 /**
  * Get posts of a user
diff --git a/src/api/endpoints/users/recommendation.ts b/src/api/endpoints/users/recommendation.ts
index b80fd63ce..736233b34 100644
--- a/src/api/endpoints/users/recommendation.ts
+++ b/src/api/endpoints/users/recommendation.ts
@@ -3,8 +3,7 @@
  */
 const ms = require('ms');
 import $ from 'cafy';
-import User from '../../models/user';
-import { pack } from '../../models/user';
+import User, { pack } from '../../models/user';
 import getFriends from '../../common/get-friends';
 
 /**
diff --git a/src/api/endpoints/users/search.ts b/src/api/endpoints/users/search.ts
index 213038403..1142db9e9 100644
--- a/src/api/endpoints/users/search.ts
+++ b/src/api/endpoints/users/search.ts
@@ -3,8 +3,7 @@
  */
 import * as mongo from 'mongodb';
 import $ from 'cafy';
-import User from '../../models/user';
-import { pack } from '../../models/user';
+import User, { pack } from '../../models/user';
 import config from '../../../conf';
 const escapeRegexp = require('escape-regexp');
 
diff --git a/src/api/endpoints/users/search_by_username.ts b/src/api/endpoints/users/search_by_username.ts
index 63e206b1f..9c5e1905a 100644
--- a/src/api/endpoints/users/search_by_username.ts
+++ b/src/api/endpoints/users/search_by_username.ts
@@ -2,8 +2,7 @@
  * Module dependencies
  */
 import $ from 'cafy';
-import User from '../../models/user';
-import { pack } from '../../models/user';
+import User, { pack } from '../../models/user';
 
 /**
  * Search a user by username
diff --git a/src/api/endpoints/users/show.ts b/src/api/endpoints/users/show.ts
index a51cb619d..7aea59296 100644
--- a/src/api/endpoints/users/show.ts
+++ b/src/api/endpoints/users/show.ts
@@ -2,8 +2,7 @@
  * Module dependencies
  */
 import $ from 'cafy';
-import User from '../../models/user';
-import { pack } from '../../models/user';
+import User, { pack } from '../../models/user';
 
 /**
  * Show a user
diff --git a/src/api/private/signin.ts b/src/api/private/signin.ts
index ab6e93562..b49d25d99 100644
--- a/src/api/private/signin.ts
+++ b/src/api/private/signin.ts
@@ -2,8 +2,7 @@ import * as express from 'express';
 import * as bcrypt from 'bcryptjs';
 import * as speakeasy from 'speakeasy';
 import { default as User, IUser } from '../models/user';
-import Signin from '../models/signin';
-import { pack } from '../models/signin';
+import Signin, { pack } from '../models/signin';
 import event from '../event';
 import signin from '../common/signin';
 import config from '../../conf';
diff --git a/src/api/private/signup.ts b/src/api/private/signup.ts
index 105fe319a..392f3b1fc 100644
--- a/src/api/private/signup.ts
+++ b/src/api/private/signup.ts
@@ -3,8 +3,7 @@ import * as express from 'express';
 import * as bcrypt from 'bcryptjs';
 import recaptcha = require('recaptcha-promise');
 import { default as User, IUser } from '../models/user';
-import { validateUsername, validatePassword } from '../models/user';
-import { pack } from '../models/user';
+import { validateUsername, validatePassword }, { pack } from '../models/user';
 import generateUserToken from '../common/generate-native-user-token';
 import config from '../../conf';
 
diff --git a/src/api/service/twitter.ts b/src/api/service/twitter.ts
index ca4f8abcc..7d4964eba 100644
--- a/src/api/service/twitter.ts
+++ b/src/api/service/twitter.ts
@@ -5,8 +5,7 @@ import * as uuid from 'uuid';
 // const Twitter = require('twitter');
 import autwh from 'autwh';
 import redis from '../../db/redis';
-import User from '../models/user';
-import { pack } from '../models/user';
+import User, { pack } from '../models/user';
 import event from '../event';
 import config from '../../conf';
 import signin from '../common/signin';

From bcd65d290d25219631bb47570478378a698d0fa0 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 4 Feb 2018 14:52:33 +0900
Subject: [PATCH 186/186] wip

---
 src/api/endpoints/aggregation/posts/reactions.ts | 11 +++++++----
 src/api/endpoints/aggregation/users.ts           | 13 ++++++++-----
 src/api/endpoints/app/create.ts                  |  3 +--
 src/api/endpoints/channels/show.ts               |  2 +-
 src/api/endpoints/drive/files/create.ts          |  2 +-
 src/api/endpoints/drive/files/update.ts          |  3 +--
 src/api/endpoints/drive/files/upload_from_url.ts |  2 +-
 src/api/endpoints/drive/folders/create.ts        |  3 +--
 src/api/endpoints/drive/folders/update.ts        |  3 +--
 src/api/endpoints/following/create.ts            |  7 +++----
 src/api/endpoints/following/delete.ts            |  5 ++---
 src/api/endpoints/i/update.ts                    |  3 +--
 src/api/endpoints/posts/reactions/create.ts      |  9 ++++-----
 src/api/endpoints/users/posts.ts                 |  4 ++--
 src/api/endpoints/users/search.ts                |  2 +-
 src/api/models/app.ts                            |  1 +
 src/api/models/drive-file.ts                     |  1 +
 src/api/models/post-reaction.ts                  |  3 +++
 src/api/models/post.ts                           |  4 +++-
 src/api/models/user.ts                           |  1 +
 src/api/private/signup.ts                        |  3 +--
 src/api/service/twitter.ts                       |  2 +-
 src/api/stream/home.ts                           |  4 ++--
 23 files changed, 48 insertions(+), 43 deletions(-)

diff --git a/src/api/endpoints/aggregation/posts/reactions.ts b/src/api/endpoints/aggregation/posts/reactions.ts
index 2cd4588ae..790b523be 100644
--- a/src/api/endpoints/aggregation/posts/reactions.ts
+++ b/src/api/endpoints/aggregation/posts/reactions.ts
@@ -35,10 +35,13 @@ module.exports = (params) => new Promise(async (res, rej) => {
 				{ deleted_at: { $gt: startTime } }
 			]
 		}, {
-			_id: false,
-			post_id: false
-		}, {
-			sort: { created_at: -1 }
+			sort: {
+				_id: -1
+			},
+			fields: {
+				_id: false,
+				post_id: false
+			}
 		});
 
 	const graph = [];
diff --git a/src/api/endpoints/aggregation/users.ts b/src/api/endpoints/aggregation/users.ts
index 9eb2d035e..e38ce92ff 100644
--- a/src/api/endpoints/aggregation/users.ts
+++ b/src/api/endpoints/aggregation/users.ts
@@ -17,11 +17,14 @@ module.exports = params => new Promise(async (res, rej) => {
 
 	const users = await User
 		.find({}, {
-			_id: false,
-			created_at: true,
-			deleted_at: true
-		}, {
-			sort: { created_at: -1 }
+			sort: {
+				_id: -1
+			},
+			fields: {
+				_id: false,
+				created_at: true,
+				deleted_at: true
+			}
 		});
 
 	const graph = [];
diff --git a/src/api/endpoints/app/create.ts b/src/api/endpoints/app/create.ts
index 71633f7de..0f688792a 100644
--- a/src/api/endpoints/app/create.ts
+++ b/src/api/endpoints/app/create.ts
@@ -3,8 +3,7 @@
  */
 import rndstr from 'rndstr';
 import $ from 'cafy';
-import App from '../../models/app';
-import { isValidNameId }, { pack } from '../../models/app';
+import App, { isValidNameId, pack } from '../../models/app';
 
 /**
  * @swagger
diff --git a/src/api/endpoints/channels/show.ts b/src/api/endpoints/channels/show.ts
index 3238616fa..332da6467 100644
--- a/src/api/endpoints/channels/show.ts
+++ b/src/api/endpoints/channels/show.ts
@@ -2,7 +2,7 @@
  * Module dependencies
  */
 import $ from 'cafy';
-import { default as Channel, IChannel }, { pack } from '../../models/channel';
+import Channel, { IChannel, pack } from '../../models/channel';
 
 /**
  * Show a channel
diff --git a/src/api/endpoints/drive/files/create.ts b/src/api/endpoints/drive/files/create.ts
index 7b424f3f5..96bcace88 100644
--- a/src/api/endpoints/drive/files/create.ts
+++ b/src/api/endpoints/drive/files/create.ts
@@ -2,7 +2,7 @@
  * Module dependencies
  */
 import $ from 'cafy';
-import { validateFileName }, { pack } from '../../../models/drive-file';
+import { validateFileName, pack } from '../../../models/drive-file';
 import create from '../../../common/add-file-to-drive';
 
 /**
diff --git a/src/api/endpoints/drive/files/update.ts b/src/api/endpoints/drive/files/update.ts
index ff65a48f7..83da46211 100644
--- a/src/api/endpoints/drive/files/update.ts
+++ b/src/api/endpoints/drive/files/update.ts
@@ -3,8 +3,7 @@
  */
 import $ from 'cafy';
 import DriveFolder from '../../../models/drive-folder';
-import DriveFile from '../../../models/drive-file';
-import { validateFileName }, { pack } from '../../../models/drive-file';
+import DriveFile, { validateFileName, pack } from '../../../models/drive-file';
 import { publishDriveStream } from '../../../event';
 
 /**
diff --git a/src/api/endpoints/drive/files/upload_from_url.ts b/src/api/endpoints/drive/files/upload_from_url.ts
index 009f06aaa..68428747e 100644
--- a/src/api/endpoints/drive/files/upload_from_url.ts
+++ b/src/api/endpoints/drive/files/upload_from_url.ts
@@ -3,7 +3,7 @@
  */
 import * as URL from 'url';
 import $ from 'cafy';
-import { validateFileName }, { pack } from '../../../models/drive-file';
+import { validateFileName, pack } from '../../../models/drive-file';
 import create from '../../../common/add-file-to-drive';
 import * as debug from 'debug';
 import * as tmp from 'tmp';
diff --git a/src/api/endpoints/drive/folders/create.ts b/src/api/endpoints/drive/folders/create.ts
index 6543b1127..03f396ddc 100644
--- a/src/api/endpoints/drive/folders/create.ts
+++ b/src/api/endpoints/drive/folders/create.ts
@@ -2,8 +2,7 @@
  * Module dependencies
  */
 import $ from 'cafy';
-import DriveFolder from '../../../models/drive-folder';
-import { isValidFolderName }, { pack } from '../../../models/drive-folder';
+import DriveFolder, { isValidFolderName, pack } from '../../../models/drive-folder';
 import { publishDriveStream } from '../../../event';
 
 /**
diff --git a/src/api/endpoints/drive/folders/update.ts b/src/api/endpoints/drive/folders/update.ts
index 2adcadcb0..d3df8bdae 100644
--- a/src/api/endpoints/drive/folders/update.ts
+++ b/src/api/endpoints/drive/folders/update.ts
@@ -2,8 +2,7 @@
  * Module dependencies
  */
 import $ from 'cafy';
-import DriveFolder from '../../../models/drive-folder';
-import { isValidFolderName }, { pack } from '../../../models/drive-folder';
+import DriveFolder, { isValidFolderName, pack } from '../../../models/drive-folder';
 import { publishDriveStream } from '../../../event';
 
 /**
diff --git a/src/api/endpoints/following/create.ts b/src/api/endpoints/following/create.ts
index b4a2217b1..8e1aa3471 100644
--- a/src/api/endpoints/following/create.ts
+++ b/src/api/endpoints/following/create.ts
@@ -2,11 +2,10 @@
  * Module dependencies
  */
 import $ from 'cafy';
-import User from '../../models/user';
+import User, { pack as packUser } from '../../models/user';
 import Following from '../../models/following';
 import notify from '../../common/notify';
 import event from '../../event';
-import serializeUser from '../../serializers/user';
 
 /**
  * Follow a user
@@ -77,8 +76,8 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 	});
 
 	// Publish follow event
-	event(follower._id, 'follow', await serializeUser(followee, follower));
-	event(followee._id, 'followed', await serializeUser(follower, followee));
+	event(follower._id, 'follow', await packUser(followee, follower));
+	event(followee._id, 'followed', await packUser(follower, followee));
 
 	// Notify
 	notify(followee._id, follower._id, 'follow');
diff --git a/src/api/endpoints/following/delete.ts b/src/api/endpoints/following/delete.ts
index aa1639ef6..b68cec09d 100644
--- a/src/api/endpoints/following/delete.ts
+++ b/src/api/endpoints/following/delete.ts
@@ -2,10 +2,9 @@
  * Module dependencies
  */
 import $ from 'cafy';
-import User from '../../models/user';
+import User, { pack as packUser } from '../../models/user';
 import Following from '../../models/following';
 import event from '../../event';
-import serializeUser from '../../serializers/user';
 
 /**
  * Unfollow a user
@@ -78,5 +77,5 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 	});
 
 	// Publish follow event
-	event(follower._id, 'unfollow', await serializeUser(followee, follower));
+	event(follower._id, 'unfollow', await packUser(followee, follower));
 });
diff --git a/src/api/endpoints/i/update.ts b/src/api/endpoints/i/update.ts
index cd4b1a13f..7bbbf9590 100644
--- a/src/api/endpoints/i/update.ts
+++ b/src/api/endpoints/i/update.ts
@@ -2,8 +2,7 @@
  * Module dependencies
  */
 import $ from 'cafy';
-import User from '../../models/user';
-import { isValidName, isValidDescription, isValidLocation, isValidBirthday }, { pack } from '../../models/user';
+import User, { isValidName, isValidDescription, isValidLocation, isValidBirthday, pack } from '../../models/user';
 import event from '../../event';
 import config from '../../../conf';
 
diff --git a/src/api/endpoints/posts/reactions/create.ts b/src/api/endpoints/posts/reactions/create.ts
index d537463df..0b0e0e294 100644
--- a/src/api/endpoints/posts/reactions/create.ts
+++ b/src/api/endpoints/posts/reactions/create.ts
@@ -3,13 +3,12 @@
  */
 import $ from 'cafy';
 import Reaction from '../../../models/post-reaction';
-import Post from '../../../models/post';
+import Post, { pack as packPost } from '../../../models/post';
+import { pack as packUser } from '../../../models/user';
 import Watching from '../../../models/post-watching';
 import notify from '../../../common/notify';
 import watch from '../../../common/watch-post';
 import { publishPostStream, pushSw } from '../../../event';
-import serializePost from '../../../serializers/post';
-import serializeUser from '../../../serializers/user';
 
 /**
  * React to a post
@@ -90,8 +89,8 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 	});
 
 	pushSw(post.user_id, 'reaction', {
-		user: await serializeUser(user, post.user_id),
-		post: await serializePost(post, post.user_id),
+		user: await packUser(user, post.user_id),
+		post: await packPost(post, post.user_id),
 		reaction: reaction
 	});
 
diff --git a/src/api/endpoints/users/posts.ts b/src/api/endpoints/users/posts.ts
index 285e5bc46..0c8bceee3 100644
--- a/src/api/endpoints/users/posts.ts
+++ b/src/api/endpoints/users/posts.ts
@@ -2,8 +2,8 @@
  * Module dependencies
  */
 import $ from 'cafy';
-import Post from '../../models/post';
-import User, { pack } from '../../models/user';
+import Post, { pack } from '../../models/post';
+import User from '../../models/user';
 
 /**
  * Get posts of a user
diff --git a/src/api/endpoints/users/search.ts b/src/api/endpoints/users/search.ts
index 1142db9e9..39e2ff989 100644
--- a/src/api/endpoints/users/search.ts
+++ b/src/api/endpoints/users/search.ts
@@ -51,7 +51,7 @@ async function byNative(res, rej, me, query, offset, max) {
 
 	// Serialize
 	res(await Promise.all(users.map(async user =>
-		await serialize(user, me, { detail: true }))));
+		await pack(user, me, { detail: true }))));
 }
 
 // Search by Elasticsearch
diff --git a/src/api/models/app.ts b/src/api/models/app.ts
index fe9d49ff6..34e9867db 100644
--- a/src/api/models/app.ts
+++ b/src/api/models/app.ts
@@ -14,6 +14,7 @@ export type IApp = {
 	_id: mongo.ObjectID;
 	created_at: Date;
 	user_id: mongo.ObjectID;
+	secret: string;
 };
 
 export function isValidNameId(nameId: string): boolean {
diff --git a/src/api/models/drive-file.ts b/src/api/models/drive-file.ts
index 9b9df1dac..2a46d8dc4 100644
--- a/src/api/models/drive-file.ts
+++ b/src/api/models/drive-file.ts
@@ -23,6 +23,7 @@ export type IDriveFile = {
 	uploadDate: Date;
 	md5: string;
 	filename: string;
+	contentType: string;
 	metadata: {
 		properties: any;
 		user_id: mongodb.ObjectID;
diff --git a/src/api/models/post-reaction.ts b/src/api/models/post-reaction.ts
index 568bfc89a..639a70e00 100644
--- a/src/api/models/post-reaction.ts
+++ b/src/api/models/post-reaction.ts
@@ -9,6 +9,9 @@ export default PostReaction;
 
 export interface IPostReaction {
 	_id: mongo.ObjectID;
+	created_at: Date;
+	deleted_at: Date;
+	reaction: string;
 }
 
 /**
diff --git a/src/api/models/post.ts b/src/api/models/post.ts
index ecc5e1a5e..0bbacebf6 100644
--- a/src/api/models/post.ts
+++ b/src/api/models/post.ts
@@ -25,10 +25,12 @@ export type IPost = {
 	media_ids: mongo.ObjectID[];
 	reply_id: mongo.ObjectID;
 	repost_id: mongo.ObjectID;
-	poll: {}; // todo
+	poll: any; // todo
 	text: string;
 	user_id: mongo.ObjectID;
 	app_id: mongo.ObjectID;
+	category: string;
+	is_category_verified: boolean;
 };
 
 /**
diff --git a/src/api/models/user.ts b/src/api/models/user.ts
index 48a45ac2f..e92f244dd 100644
--- a/src/api/models/user.ts
+++ b/src/api/models/user.ts
@@ -42,6 +42,7 @@ export function isValidBirthday(birthday: string): boolean {
 export type IUser = {
 	_id: mongo.ObjectID;
 	created_at: Date;
+	deleted_at: Date;
 	email: string;
 	followers_count: number;
 	following_count: number;
diff --git a/src/api/private/signup.ts b/src/api/private/signup.ts
index 392f3b1fc..8efdb6db4 100644
--- a/src/api/private/signup.ts
+++ b/src/api/private/signup.ts
@@ -2,8 +2,7 @@ import * as uuid from 'uuid';
 import * as express from 'express';
 import * as bcrypt from 'bcryptjs';
 import recaptcha = require('recaptcha-promise');
-import { default as User, IUser } from '../models/user';
-import { validateUsername, validatePassword }, { pack } from '../models/user';
+import User, { IUser, validateUsername, validatePassword, pack } from '../models/user';
 import generateUserToken from '../common/generate-native-user-token';
 import config from '../../conf';
 
diff --git a/src/api/service/twitter.ts b/src/api/service/twitter.ts
index 7d4964eba..adcd5ac49 100644
--- a/src/api/service/twitter.ts
+++ b/src/api/service/twitter.ts
@@ -163,7 +163,7 @@ module.exports = (app: express.Application) => {
 				res.send(`Twitter: @${result.screenName} を、Misskey: @${user.username} に接続しました!`);
 
 				// Publish i updated event
-				event(user._id, 'i_updated', await serialize(user, user, {
+				event(user._id, 'i_updated', await pack(user, user, {
 					detail: true,
 					includeSecrets: true
 				}));
diff --git a/src/api/stream/home.ts b/src/api/stream/home.ts
index 7dcdb5ed7..10078337c 100644
--- a/src/api/stream/home.ts
+++ b/src/api/stream/home.ts
@@ -4,7 +4,7 @@ import * as debug from 'debug';
 
 import User from '../models/user';
 import Mute from '../models/mute';
-import serializePost from '../serializers/post';
+import { pack as packPost } from '../models/post';
 import readNotification from '../common/read-notification';
 
 const log = debug('misskey');
@@ -49,7 +49,7 @@ export default async function(request: websocket.request, connection: websocket.
 			case 'post-stream':
 				const postId = channel.split(':')[2];
 				log(`RECEIVED: ${postId} ${data} by @${user.username}`);
-				const post = await serializePost(postId, user, {
+				const post = await packPost(postId, user, {
 					detail: true
 				});
 				connection.send(JSON.stringify({