From a478a620cc494d8f0516ffb4c6a807dbaec0facc Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Tue, 10 Apr 2018 20:53:29 +0000
Subject: [PATCH 01/34] fix(package): update @types/node to version 9.6.4

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

diff --git a/package.json b/package.json
index 2a60a794ba..2193ff3c89 100644
--- a/package.json
+++ b/package.json
@@ -65,7 +65,7 @@
 		"@types/morgan": "1.7.35",
 		"@types/ms": "0.7.30",
 		"@types/multer": "1.3.6",
-		"@types/node": "9.6.2",
+		"@types/node": "9.6.4",
 		"@types/nopt": "3.0.29",
 		"@types/proxy-addr": "2.0.0",
 		"@types/pug": "2.0.4",

From 4166fd87c241f651932f99fd2909fa44acee4446 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Wed, 11 Apr 2018 18:24:42 +0900
Subject: [PATCH 02/34] wip #1443

---
 src/models/note.ts | 32 ++++++++++++++++++++++++++++++++
 src/models/user.ts | 36 +++++++++++++++++++++++++++++++++++-
 2 files changed, 67 insertions(+), 1 deletion(-)

diff --git a/src/models/note.ts b/src/models/note.ts
index f509fa66c8..a11da196cd 100644
--- a/src/models/note.ts
+++ b/src/models/note.ts
@@ -69,6 +69,38 @@ export type INote = {
 	};
 };
 
+// TODO
+export async function physicalDelete(note: string | mongo.ObjectID | INote) {
+	let n: INote;
+
+	// Populate
+	if (mongo.ObjectID.prototype.isPrototypeOf(note)) {
+		n = await Note.findOne({
+			_id: note
+		});
+	} else if (typeof note === 'string') {
+		n = await Note.findOne({
+			_id: new mongo.ObjectID(note)
+		});
+	} else {
+		n = note as INote;
+	}
+
+	if (n == null) return;
+
+	// この投稿の返信をすべて削除
+	const replies = await Note.find({
+		replyId: n._id
+	});
+	await Promise.all(replies.map(r => physicalDelete(r)));
+
+	// この投稿のWatchをすべて削除
+
+	// この投稿のReactionをすべて削除
+
+	// この投稿に対するFavoriteをすべて削除
+}
+
 /**
  * Pack a note for API response
  *
diff --git a/src/models/user.ts b/src/models/user.ts
index adc9e6da99..a2800a3808 100644
--- a/src/models/user.ts
+++ b/src/models/user.ts
@@ -2,7 +2,7 @@ import * as mongo from 'mongodb';
 import deepcopy = require('deepcopy');
 import rap from '@prezzemolo/rap';
 import db from '../db/mongodb';
-import { INote, pack as packNote } from './note';
+import Note, { INote, pack as packNote, physicalDelete as physicalDeleteNote } from './note';
 import Following from './following';
 import Mute from './mute';
 import getFriends from '../server/api/common/get-friends';
@@ -121,6 +121,40 @@ export function init(user): IUser {
 	return user;
 }
 
+// TODO
+export async function physicalDelete(user: string | mongo.ObjectID | IUser) {
+	let u: IUser;
+
+	// Populate
+	if (mongo.ObjectID.prototype.isPrototypeOf(user)) {
+		u = await User.findOne({
+			_id: user
+		});
+	} else if (typeof user === 'string') {
+		u = await User.findOne({
+			_id: new mongo.ObjectID(user)
+		});
+	} else {
+		u = user as IUser;
+	}
+
+	if (u == null) return;
+
+	// このユーザーが行った投稿をすべて削除
+	const notes = await Note.find({ userId: u._id });
+	await Promise.all(notes.map(n => physicalDeleteNote(n)));
+
+	// このユーザーのお気に入りをすべて削除
+
+	// このユーザーが行ったメッセージをすべて削除
+
+	// このユーザーのドライブのファイルをすべて削除
+
+	// このユーザーに関するfollowingをすべて削除
+
+	// このユーザーを削除
+}
+
 /**
  * Pack a user for API response
  *

From 3569a7820ef233adc2ef8cd9914724e8cd1f19f6 Mon Sep 17 00:00:00 2001
From: tamaina <tamaina@hotmail.co.jp>
Date: Wed, 11 Apr 2018 21:05:47 +0900
Subject: [PATCH 03/34] =?UTF-8?q?=E3=83=A6=E3=83=BC=E3=82=B6=E3=83=BC?=
 =?UTF-8?q?=E5=90=8D=E3=81=AE=E5=B0=91=E3=81=AA=E3=81=8F=E3=81=A8=E3=82=82?=
 =?UTF-8?q?3=E6=96=87=E5=AD=97=E4=BB=A5=E4=B8=8A=E3=81=A8=E3=81=84?=
 =?UTF-8?q?=E3=81=86=E5=88=B6=E9=99=90=E3=82=92=E6=92=A4=E5=BB=83=E3=81=A7?=
 =?UTF-8?q?=E3=81=8D=E3=81=A6=E3=81=84=E3=81=AA=E3=81=8B=E3=81=A3=E3=81=9F?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 src/client/app/common/views/components/signup.vue | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/client/app/common/views/components/signup.vue b/src/client/app/common/views/components/signup.vue
index 30fe7b7ad0..40262b54d3 100644
--- a/src/client/app/common/views/components/signup.vue
+++ b/src/client/app/common/views/components/signup.vue
@@ -77,7 +77,7 @@ export default Vue.extend({
 
 			const err =
 				!this.username.match(/^[a-zA-Z0-9_]+$/) ? 'invalid-format' :
-				this.username.length < 3 ? 'min-range' :
+				this.username.length < 1 ? 'min-range' :
 				this.username.length > 20 ? 'max-range' :
 				null;
 

From 39d87305bae56e90fadabff52424872c6c2b6fc6 Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Thu, 12 Apr 2018 02:48:06 +0900
Subject: [PATCH 04/34] Update index.ts

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

diff --git a/src/server/file/index.ts b/src/server/file/index.ts
index 658117e3ac..95e7867f01 100644
--- a/src/server/file/index.ts
+++ b/src/server/file/index.ts
@@ -56,11 +56,12 @@ function thumbnail(data: stream.Readable, type: string, resize: number): ISend {
 	const readable: stream.Readable = (() => {
 		// 動画であれば
 		if (/^video\/.*$/.test(type)) {
-			// 実装は先延ばし
+			// TODO
 			// 使わないことになったストリームはしっかり取り壊す
 			data.destroy();
 			return fs.createReadStream(`${__dirname}/assets/thumbnail-not-available.png`);
 		// 画像であれば
+		// Note: SVGはapplication/xml
 		} else if (/^image\/.*$/.test(type) || type == 'application/xml') {
 			// 0フレーム目を送る
 			try {

From c32385b616ba431a46331138b630fcc427039ee9 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 12 Apr 2018 02:53:48 +0900
Subject: [PATCH 05/34] Add missing index

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

diff --git a/src/models/user.ts b/src/models/user.ts
index a2800a3808..cdf9a564fc 100644
--- a/src/models/user.ts
+++ b/src/models/user.ts
@@ -11,6 +11,7 @@ import config from '../config';
 const User = db.get<IUser>('users');
 
 User.createIndex('username');
+User.createIndex('usernameLower');
 User.createIndex('token');
 User.createIndex('uri', { sparse: true, unique: true });
 

From 553fccd719690a659f5985e7956e875af92d17e0 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 12 Apr 2018 03:46:32 +0900
Subject: [PATCH 06/34] wip

---
 src/models/access-token.ts  | 31 +++++++++++++++++++++--
 src/models/favorite.ts      | 31 +++++++++++++++++++++--
 src/models/note-reaction.ts | 31 ++++++++++++++++++++---
 src/models/note-watching.ts | 27 ++++++++++++++++++++
 src/models/note.ts          | 43 ++++++++++++++++++++++++--------
 src/models/user.ts          | 49 +++++++++++++++++++++++++++++--------
 6 files changed, 184 insertions(+), 28 deletions(-)

diff --git a/src/models/access-token.ts b/src/models/access-token.ts
index 4451ca140d..9909ea01ad 100644
--- a/src/models/access-token.ts
+++ b/src/models/access-token.ts
@@ -1,12 +1,12 @@
 import * as mongo from 'mongodb';
 import db from '../db/mongodb';
 
-const AccessToken = db.get<IAccessTokens>('accessTokens');
+const AccessToken = db.get<IAccessToken>('accessTokens');
 AccessToken.createIndex('token');
 AccessToken.createIndex('hash');
 export default AccessToken;
 
-export type IAccessTokens = {
+export type IAccessToken = {
 	_id: mongo.ObjectID;
 	createdAt: Date;
 	appId: mongo.ObjectID;
@@ -14,3 +14,30 @@ export type IAccessTokens = {
 	token: string;
 	hash: string;
 };
+
+/**
+ * AccessTokenを物理削除します
+ */
+export async function deleteAccessToken(accessToken: string | mongo.ObjectID | IAccessToken) {
+	let a: IAccessToken;
+
+	// Populate
+	if (mongo.ObjectID.prototype.isPrototypeOf(accessToken)) {
+		a = await AccessToken.findOne({
+			_id: accessToken
+		});
+	} else if (typeof accessToken === 'string') {
+		a = await AccessToken.findOne({
+			_id: new mongo.ObjectID(accessToken)
+		});
+	} else {
+		a = accessToken as IAccessToken;
+	}
+
+	if (a == null) return;
+
+	// このAccessTokenを削除
+	await AccessToken.remove({
+		_id: a._id
+	});
+}
diff --git a/src/models/favorite.ts b/src/models/favorite.ts
index 73f8881926..b2c5828088 100644
--- a/src/models/favorite.ts
+++ b/src/models/favorite.ts
@@ -1,8 +1,8 @@
 import * as mongo from 'mongodb';
 import db from '../db/mongodb';
 
-const Favorites = db.get<IFavorite>('favorites');
-export default Favorites;
+const Favorite = db.get<IFavorite>('favorites');
+export default Favorite;
 
 export type IFavorite = {
 	_id: mongo.ObjectID;
@@ -10,3 +10,30 @@ export type IFavorite = {
 	userId: mongo.ObjectID;
 	noteId: mongo.ObjectID;
 };
+
+/**
+ * Favoriteを物理削除します
+ */
+export async function deleteFavorite(favorite: string | mongo.ObjectID | IFavorite) {
+	let f: IFavorite;
+
+	// Populate
+	if (mongo.ObjectID.prototype.isPrototypeOf(favorite)) {
+		f = await Favorite.findOne({
+			_id: favorite
+		});
+	} else if (typeof favorite === 'string') {
+		f = await Favorite.findOne({
+			_id: new mongo.ObjectID(favorite)
+		});
+	} else {
+		f = favorite as IFavorite;
+	}
+
+	if (f == null) return;
+
+	// このFavoriteを削除
+	await Favorite.remove({
+		_id: f._id
+	});
+}
diff --git a/src/models/note-reaction.ts b/src/models/note-reaction.ts
index d499442de9..9bf467f222 100644
--- a/src/models/note-reaction.ts
+++ b/src/models/note-reaction.ts
@@ -16,12 +16,35 @@ export interface INoteReaction {
 	reaction: string;
 }
 
+/**
+ * NoteReactionを物理削除します
+ */
+export async function deleteNoteReaction(noteReaction: string | mongo.ObjectID | INoteReaction) {
+	let n: INoteReaction;
+
+	// Populate
+	if (mongo.ObjectID.prototype.isPrototypeOf(noteReaction)) {
+		n = await NoteReaction.findOne({
+			_id: noteReaction
+		});
+	} else if (typeof noteReaction === 'string') {
+		n = await NoteReaction.findOne({
+			_id: new mongo.ObjectID(noteReaction)
+		});
+	} else {
+		n = noteReaction as INoteReaction;
+	}
+
+	if (n == null) return;
+
+	// このNoteReactionを削除
+	await NoteReaction.remove({
+		_id: n._id
+	});
+}
+
 /**
  * Pack a reaction for API response
- *
- * @param {any} reaction
- * @param {any} me?
- * @return {Promise<any>}
  */
 export const pack = (
 	reaction: any,
diff --git a/src/models/note-watching.ts b/src/models/note-watching.ts
index b5ef3b61b7..479f92dd44 100644
--- a/src/models/note-watching.ts
+++ b/src/models/note-watching.ts
@@ -11,3 +11,30 @@ export interface INoteWatching {
 	userId: mongo.ObjectID;
 	noteId: mongo.ObjectID;
 }
+
+/**
+ * NoteWatchingを物理削除します
+ */
+export async function deleteNoteWatching(noteWatching: string | mongo.ObjectID | INoteWatching) {
+	let n: INoteWatching;
+
+	// Populate
+	if (mongo.ObjectID.prototype.isPrototypeOf(noteWatching)) {
+		n = await NoteWatching.findOne({
+			_id: noteWatching
+		});
+	} else if (typeof noteWatching === 'string') {
+		n = await NoteWatching.findOne({
+			_id: new mongo.ObjectID(noteWatching)
+		});
+	} else {
+		n = noteWatching as INoteWatching;
+	}
+
+	if (n == null) return;
+
+	// このNoteWatchingを削除
+	await NoteWatching.remove({
+		_id: n._id
+	});
+}
diff --git a/src/models/note.ts b/src/models/note.ts
index a11da196cd..6e7b6cee79 100644
--- a/src/models/note.ts
+++ b/src/models/note.ts
@@ -6,8 +6,11 @@ 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 './note-reaction';
+import Reaction, { deleteNoteReaction } from './note-reaction';
 import { pack as packFile } from './drive-file';
+import NoteWatching, { deleteNoteWatching } from './note-watching';
+import NoteReaction from './note-reaction';
+import Favorite, { deleteFavorite } from './favorite';
 
 const Note = db.get<INote>('notes');
 
@@ -69,8 +72,10 @@ export type INote = {
 	};
 };
 
-// TODO
-export async function physicalDelete(note: string | mongo.ObjectID | INote) {
+/**
+ * Noteを物理削除します
+ */
+export async function deleteNote(note: string | mongo.ObjectID | INote) {
 	let n: INote;
 
 	// Populate
@@ -88,17 +93,35 @@ export async function physicalDelete(note: string | mongo.ObjectID | INote) {
 
 	if (n == null) return;
 
-	// この投稿の返信をすべて削除
-	const replies = await Note.find({
-		replyId: n._id
-	});
-	await Promise.all(replies.map(r => physicalDelete(r)));
+	// このNoteへの返信をすべて削除
+	await Promise.all((
+		await Note.find({ replyId: n._id })
+	).map(x => deleteNote(x)));
 
-	// この投稿のWatchをすべて削除
+	// このNoteのRenoteをすべて削除
+	await Promise.all((
+		await Note.find({ renoteId: n._id })
+	).map(x => deleteNote(x)));
 
-	// この投稿のReactionをすべて削除
+	// この投稿に対するNoteWatchingをすべて削除
+	await Promise.all((
+		await NoteWatching.find({ noteId: n._id })
+	).map(x => deleteNoteWatching(x)));
+
+	// この投稿に対するNoteReactionをすべて削除
+	await Promise.all((
+		await NoteReaction.find({ noteId: n._id })
+	).map(x => deleteNoteReaction(x)));
 
 	// この投稿に対するFavoriteをすべて削除
+	await Promise.all((
+		await Favorite.find({ noteId: n._id })
+	).map(x => deleteFavorite(x)));
+
+	// このNoteを削除
+	await Note.remove({
+		_id: n._id
+	});
 }
 
 /**
diff --git a/src/models/user.ts b/src/models/user.ts
index cdf9a564fc..b1a68b0827 100644
--- a/src/models/user.ts
+++ b/src/models/user.ts
@@ -2,11 +2,15 @@ import * as mongo from 'mongodb';
 import deepcopy = require('deepcopy');
 import rap from '@prezzemolo/rap';
 import db from '../db/mongodb';
-import Note, { INote, pack as packNote, physicalDelete as physicalDeleteNote } from './note';
+import Note, { INote, pack as packNote, deleteNote } from './note';
 import Following from './following';
 import Mute from './mute';
 import getFriends from '../server/api/common/get-friends';
 import config from '../config';
+import AccessToken, { deleteAccessToken } from './access-token';
+import NoteWatching, { deleteNoteWatching } from './note-watching';
+import Favorite, { deleteFavorite } from './favorite';
+import NoteReaction, { deleteNoteReaction } from './note-reaction';
 
 const User = db.get<IUser>('users');
 
@@ -122,8 +126,10 @@ export function init(user): IUser {
 	return user;
 }
 
-// TODO
-export async function physicalDelete(user: string | mongo.ObjectID | IUser) {
+/**
+ * Userを物理削除します
+ */
+export async function deleteUser(user: string | mongo.ObjectID | IUser) {
 	let u: IUser;
 
 	// Populate
@@ -141,17 +147,40 @@ export async function physicalDelete(user: string | mongo.ObjectID | IUser) {
 
 	if (u == null) return;
 
-	// このユーザーが行った投稿をすべて削除
-	const notes = await Note.find({ userId: u._id });
-	await Promise.all(notes.map(n => physicalDeleteNote(n)));
+	// このユーザーのAccessTokenをすべて削除
+	await Promise.all((
+		await AccessToken.find({ userId: u._id })
+	).map(x => deleteAccessToken(x)));
 
-	// このユーザーのお気に入りをすべて削除
+	// このユーザーのNoteをすべて削除
+	await Promise.all((
+		await Note.find({ userId: u._id })
+	).map(x => deleteNote(x)));
 
-	// このユーザーが行ったメッセージをすべて削除
+	// このユーザーのNoteReactionをすべて削除
+	await Promise.all((
+		await NoteReaction.find({ userId: u._id })
+	).map(x => deleteNoteReaction(x)));
 
-	// このユーザーのドライブのファイルをすべて削除
+	// このユーザーのNoteWatchingをすべて削除
+	await Promise.all((
+		await NoteWatching.find({ userId: u._id })
+	).map(x => deleteNoteWatching(x)));
 
-	// このユーザーに関するfollowingをすべて削除
+	// このユーザーのFavoriteをすべて削除
+	await Promise.all((
+		await Favorite.find({ userId: u._id })
+	).map(x => deleteFavorite(x)));
+
+	// このユーザーのMessageをすべて削除
+
+	// このユーザーへのMessageをすべて削除
+
+	// このユーザーのDriveFileをすべて削除
+
+	// このユーザーのFollowingをすべて削除
+
+	// このユーザーへのFollowingをすべて削除
 
 	// このユーザーを削除
 }

From 92dd4b3e5a701823ebb5a453aa42202da8a326ea Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 12 Apr 2018 04:05:03 +0900
Subject: [PATCH 07/34] wip

---
 src/models/messaging-history.ts | 27 +++++++++++++++++++++++
 src/models/messaging-message.ts | 38 ++++++++++++++++++++++++++++-----
 src/models/user.ts              | 13 +++++++++++
 3 files changed, 73 insertions(+), 5 deletions(-)

diff --git a/src/models/messaging-history.ts b/src/models/messaging-history.ts
index 6864e22d2f..5367f81412 100644
--- a/src/models/messaging-history.ts
+++ b/src/models/messaging-history.ts
@@ -11,3 +11,30 @@ export type IMessagingHistory = {
 	partnerId: mongo.ObjectID;
 	messageId: mongo.ObjectID;
 };
+
+/**
+ * MessagingHistoryを物理削除します
+ */
+export async function deleteMessagingHistory(messagingHistory: string | mongo.ObjectID | IMessagingHistory) {
+	let m: IMessagingHistory;
+
+	// Populate
+	if (mongo.ObjectID.prototype.isPrototypeOf(messagingHistory)) {
+		m = await MessagingHistory.findOne({
+			_id: messagingHistory
+		});
+	} else if (typeof messagingHistory === 'string') {
+		m = await MessagingHistory.findOne({
+			_id: new mongo.ObjectID(messagingHistory)
+		});
+	} else {
+		m = messagingHistory as IMessagingHistory;
+	}
+
+	if (m == null) return;
+
+	// このMessagingHistoryを削除
+	await MessagingHistory.remove({
+		_id: m._id
+	});
+}
diff --git a/src/models/messaging-message.ts b/src/models/messaging-message.ts
index 974ee54ab8..9d62fab4fa 100644
--- a/src/models/messaging-message.ts
+++ b/src/models/messaging-message.ts
@@ -3,6 +3,7 @@ import deepcopy = require('deepcopy');
 import { pack as packUser } from './user';
 import { pack as packFile } from './drive-file';
 import db from '../db/mongodb';
+import MessagingHistory, { deleteMessagingHistory } from './messaging-history';
 
 const MessagingMessage = db.get<IMessagingMessage>('messagingMessages');
 export default MessagingMessage;
@@ -22,13 +23,40 @@ export function isValidText(text: string): boolean {
 	return text.length <= 1000 && text.trim() != '';
 }
 
+/**
+ * MessagingMessageを物理削除します
+ */
+export async function deleteMessagingMessage(messagingMessage: string | mongo.ObjectID | IMessagingMessage) {
+	let m: IMessagingMessage;
+
+	// Populate
+	if (mongo.ObjectID.prototype.isPrototypeOf(messagingMessage)) {
+		m = await MessagingMessage.findOne({
+			_id: messagingMessage
+		});
+	} else if (typeof messagingMessage === 'string') {
+		m = await MessagingMessage.findOne({
+			_id: new mongo.ObjectID(messagingMessage)
+		});
+	} else {
+		m = messagingMessage as IMessagingMessage;
+	}
+
+	if (m == null) return;
+
+	// このMessagingMessageを指すMessagingHistoryをすべて削除
+	await Promise.all((
+		await MessagingHistory.find({ messageId: m._id })
+	).map(x => deleteMessagingHistory(x)));
+
+	// このMessagingMessageを削除
+	await MessagingMessage.remove({
+		_id: m._id
+	});
+}
+
 /**
  * Pack a messaging message for API response
- *
- * @param {any} message
- * @param {any} me?
- * @param {any} options?
- * @return {Promise<any>}
  */
 export const pack = (
 	message: any,
diff --git a/src/models/user.ts b/src/models/user.ts
index b1a68b0827..6155324be8 100644
--- a/src/models/user.ts
+++ b/src/models/user.ts
@@ -11,6 +11,8 @@ import AccessToken, { deleteAccessToken } from './access-token';
 import NoteWatching, { deleteNoteWatching } from './note-watching';
 import Favorite, { deleteFavorite } from './favorite';
 import NoteReaction, { deleteNoteReaction } from './note-reaction';
+import MessagingMessage, { deleteMessagingMessage } from './messaging-message';
+import MessagingHistory, { deleteMessagingHistory } from './messaging-history';
 
 const User = db.get<IUser>('users');
 
@@ -173,8 +175,19 @@ export async function deleteUser(user: string | mongo.ObjectID | IUser) {
 	).map(x => deleteFavorite(x)));
 
 	// このユーザーのMessageをすべて削除
+	await Promise.all((
+		await MessagingMessage.find({ userId: u._id })
+	).map(x => deleteMessagingMessage(x)));
 
 	// このユーザーへのMessageをすべて削除
+	await Promise.all((
+		await MessagingMessage.find({ recipientId: u._id })
+	).map(x => deleteMessagingMessage(x)));
+
+	// このユーザーの関わるMessagingHistoryをすべて削除
+	await Promise.all((
+		await MessagingHistory.find({ $or: [{ partnerId: u._id }, { userId: u._id }] })
+	).map(x => deleteMessagingHistory(x)));
 
 	// このユーザーのDriveFileをすべて削除
 

From 53415e9ba4bbee35b337afd97940e23eb4523d2c Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 12 Apr 2018 04:22:06 +0900
Subject: [PATCH 08/34] wip

---
 src/models/drive-file.ts   | 67 ++++++++++++++++++++++++++++++++------
 src/models/drive-folder.ts | 49 +++++++++++++++++++++++++---
 src/models/user.ts         | 10 ++++++
 3 files changed, 112 insertions(+), 14 deletions(-)

diff --git a/src/models/drive-file.ts b/src/models/drive-file.ts
index c86570f0f7..ff31ba05dd 100644
--- a/src/models/drive-file.ts
+++ b/src/models/drive-file.ts
@@ -1,8 +1,11 @@
-import * as mongodb from 'mongodb';
+import * as mongo from 'mongodb';
 import deepcopy = require('deepcopy');
 import { pack as packFolder } from './drive-folder';
 import config from '../config';
 import monkDb, { nativeDbConn } from '../db/mongodb';
+import Note, { deleteNote } from './note';
+import MessagingMessage, { deleteMessagingMessage } from './messaging-message';
+import User from './user';
 
 const DriveFile = monkDb.get<IDriveFile>('driveFiles.files');
 
@@ -10,9 +13,9 @@ DriveFile.createIndex('metadata.uri', { sparse: true, unique: true });
 
 export default DriveFile;
 
-const getGridFSBucket = async (): Promise<mongodb.GridFSBucket> => {
+const getGridFSBucket = async (): Promise<mongo.GridFSBucket> => {
 	const db = await nativeDbConn();
-	const bucket = new mongodb.GridFSBucket(db, {
+	const bucket = new mongo.GridFSBucket(db, {
 		bucketName: 'driveFiles'
 	});
 	return bucket;
@@ -22,14 +25,14 @@ export { getGridFSBucket };
 
 export type IMetadata = {
 	properties: any;
-	userId: mongodb.ObjectID;
-	folderId: mongodb.ObjectID;
+	userId: mongo.ObjectID;
+	folderId: mongo.ObjectID;
 	comment: string;
 	uri: string;
 };
 
 export type IDriveFile = {
-	_id: mongodb.ObjectID;
+	_id: mongo.ObjectID;
 	uploadDate: Date;
 	md5: string;
 	filename: string;
@@ -47,12 +50,56 @@ export function validateFileName(name: string): boolean {
 	);
 }
 
+/**
+ * DriveFileを物理削除します
+ */
+export async function deleteDriveFile(driveFile: string | mongo.ObjectID | IDriveFile) {
+	let d: IDriveFile;
+
+	// Populate
+	if (mongo.ObjectID.prototype.isPrototypeOf(driveFile)) {
+		d = await DriveFile.findOne({
+			_id: driveFile
+		});
+	} else if (typeof driveFile === 'string') {
+		d = await DriveFile.findOne({
+			_id: new mongo.ObjectID(driveFile)
+		});
+	} else {
+		d = driveFile as IDriveFile;
+	}
+
+	if (d == null) return;
+
+	// このDriveFileを添付しているNoteをすべて削除
+	await Promise.all((
+		await Note.find({ mediaIds: d._id })
+	).map(x => deleteNote(x)));
+
+	// このDriveFileを添付しているMessagingMessageをすべて削除
+	await Promise.all((
+		await MessagingMessage.find({ fileId: d._id })
+	).map(x => deleteMessagingMessage(x)));
+
+	// このDriveFileがアバターやバナーに使われていたらそれらのプロパティをnullにする
+	const u = await User.findOne({ _id: d.metadata.userId });
+	if (u) {
+		if (u.avatarId.equals(d._id)) {
+			await User.update({ _id: u._id }, { $set: { avatarId: null } });
+		}
+		if (u.bannerId.equals(d._id)) {
+			await User.update({ _id: u._id }, { $set: { bannerId: null } });
+		}
+	}
+
+	// このDriveFileを削除
+	await DriveFile.remove({
+		_id: d._id
+	});
+}
+
 /**
  * Pack a drive file for API response
- *
- * @param {any} file
- * @param {any} options?
- * @return {Promise<any>}
  */
 export const pack = (
 	file: any,
diff --git a/src/models/drive-folder.ts b/src/models/drive-folder.ts
index 45cc9c9649..e7961936aa 100644
--- a/src/models/drive-folder.ts
+++ b/src/models/drive-folder.ts
@@ -21,12 +21,53 @@ export function isValidFolderName(name: string): boolean {
 	);
 }
 
+/**
+ * DriveFolderを物理削除します
+ */
+export async function deleteDriveFolder(driveFolder: string | mongo.ObjectID | IDriveFolder) {
+	let d: IDriveFolder;
+
+	// Populate
+	if (mongo.ObjectID.prototype.isPrototypeOf(driveFolder)) {
+		d = await DriveFolder.findOne({
+			_id: driveFolder
+		});
+	} else if (typeof driveFolder === 'string') {
+		d = await DriveFolder.findOne({
+			_id: new mongo.ObjectID(driveFolder)
+		});
+	} else {
+		d = driveFolder as IDriveFolder;
+	}
+
+	if (d == null) return;
+
+	// このDriveFolderに格納されているDriveFileがあればすべてルートに移動
+	await DriveFile.update({
+		'metadata.folderId': d._id
+	}, {
+		$set: {
+			'metadata.folderId': null
+		}
+	});
+
+	// このDriveFolderに格納されているDriveFolderがあればすべてルートに移動
+	await DriveFolder.update({
+		parentId: d._id
+	}, {
+		$set: {
+			parentId: null
+		}
+	});
+
+	// このDriveFolderを削除
+	await DriveFolder.remove({
+		_id: d._id
+	});
+}
+
 /**
  * Pack a drive folder for API response
- *
- * @param {any} folder
- * @param {any} options?
- * @return {Promise<any>}
  */
 export const pack = (
 	folder: any,
diff --git a/src/models/user.ts b/src/models/user.ts
index 6155324be8..b56cf03ef8 100644
--- a/src/models/user.ts
+++ b/src/models/user.ts
@@ -13,6 +13,8 @@ import Favorite, { deleteFavorite } from './favorite';
 import NoteReaction, { deleteNoteReaction } from './note-reaction';
 import MessagingMessage, { deleteMessagingMessage } from './messaging-message';
 import MessagingHistory, { deleteMessagingHistory } from './messaging-history';
+import DriveFile, { deleteDriveFile } from './drive-file';
+import DriveFolder, { deleteDriveFolder } from './drive-folder';
 
 const User = db.get<IUser>('users');
 
@@ -190,6 +192,14 @@ export async function deleteUser(user: string | mongo.ObjectID | IUser) {
 	).map(x => deleteMessagingHistory(x)));
 
 	// このユーザーのDriveFileをすべて削除
+	await Promise.all((
+		await DriveFile.find({ 'metadata.userId': u._id })
+	).map(x => deleteDriveFile(x)));
+
+	// このユーザーのDriveFolderをすべて削除
+	await Promise.all((
+		await DriveFolder.find({ userId: u._id })
+	).map(x => deleteDriveFolder(x)));
 
 	// このユーザーのFollowingをすべて削除
 

From b846eb8afe0376feacb567e86bf157b8fd5f4c36 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 12 Apr 2018 05:50:45 +0900
Subject: [PATCH 09/34] wip

---
 src/models/mute.ts | 27 +++++++++++++++++++++++++++
 src/models/user.ts | 16 +++++++++++++++-
 2 files changed, 42 insertions(+), 1 deletion(-)

diff --git a/src/models/mute.ts b/src/models/mute.ts
index 8793615967..e068215c94 100644
--- a/src/models/mute.ts
+++ b/src/models/mute.ts
@@ -11,3 +11,30 @@ export interface IMute {
 	muterId: mongo.ObjectID;
 	muteeId: mongo.ObjectID;
 }
+
+/**
+ * Muteを物理削除します
+ */
+export async function deleteMute(mute: string | mongo.ObjectID | IMute) {
+	let m: IMute;
+
+	// Populate
+	if (mongo.ObjectID.prototype.isPrototypeOf(mute)) {
+		m = await Mute.findOne({
+			_id: mute
+		});
+	} else if (typeof mute === 'string') {
+		m = await Mute.findOne({
+			_id: new mongo.ObjectID(mute)
+		});
+	} else {
+		m = mute as IMute;
+	}
+
+	if (m == null) return;
+
+	// このMuteを削除
+	await Mute.remove({
+		_id: m._id
+	});
+}
diff --git a/src/models/user.ts b/src/models/user.ts
index b56cf03ef8..ff1c11e76c 100644
--- a/src/models/user.ts
+++ b/src/models/user.ts
@@ -4,7 +4,7 @@ import rap from '@prezzemolo/rap';
 import db from '../db/mongodb';
 import Note, { INote, pack as packNote, deleteNote } from './note';
 import Following from './following';
-import Mute from './mute';
+import Mute, { deleteMute } from './mute';
 import getFriends from '../server/api/common/get-friends';
 import config from '../config';
 import AccessToken, { deleteAccessToken } from './access-token';
@@ -201,10 +201,24 @@ export async function deleteUser(user: string | mongo.ObjectID | IUser) {
 		await DriveFolder.find({ userId: u._id })
 	).map(x => deleteDriveFolder(x)));
 
+	// このユーザーのMuteをすべて削除
+	await Promise.all((
+		await Mute.find({ muterId: u._id })
+	).map(x => deleteMute(x)));
+
+	// このユーザーへのMuteをすべて削除
+	await Promise.all((
+		await Mute.find({ muteeId: u._id })
+	).map(x => deleteMute(x)));
+
 	// このユーザーのFollowingをすべて削除
 
 	// このユーザーへのFollowingをすべて削除
 
+	// このユーザーのFollowingLogをすべて削除
+
+	// このユーザーのFollowedLogをすべて削除
+
 	// このユーザーを削除
 }
 

From 0f994692435d7be4f94492294113db3b5dbc17a1 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 12 Apr 2018 05:54:54 +0900
Subject: [PATCH 10/34] HSTS

Co-Authored-By: tamaina <tamaina@hotmail.co.jp>
---
 src/server/index.ts | 11 +++++++++++
 1 file changed, 11 insertions(+)

diff --git a/src/server/index.ts b/src/server/index.ts
index abb8992da5..9358cce2ac 100644
--- a/src/server/index.ts
+++ b/src/server/index.ts
@@ -41,6 +41,17 @@ app.use((req, res, next) => {
 	next();
 });
 
+/**
+ * HSTS
+ * 6month(15552000sec)
+ */
+if (config.url.startsWith('https')) {
+	app.use((req, res, next) => {
+		res.header('strict-transport-security', 'max-age=15552000; preload');
+		next();
+	});
+}
+
 // Drop request when without 'Host' header
 app.use((req, res, next) => {
 	if (!req.headers['host']) {

From 70433469a16f6279390c8c0bdb3b9048ccfb3841 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 12 Apr 2018 06:02:34 +0900
Subject: [PATCH 11/34] Clean up

---
 package.json        |  1 -
 src/config/types.ts |  4 ----
 src/server/index.ts | 11 -----------
 3 files changed, 16 deletions(-)

diff --git a/package.json b/package.json
index 048ef651de..d5233d54a6 100644
--- a/package.json
+++ b/package.json
@@ -84,7 +84,6 @@
 		"@types/webpack-stream": "3.2.10",
 		"@types/websocket": "0.0.38",
 		"@types/ws": "4.0.2",
-		"accesses": "2.5.0",
 		"animejs": "2.2.0",
 		"autosize": "4.0.1",
 		"autwh": "0.1.0",
diff --git a/src/config/types.ts b/src/config/types.ts
index f802e70d1e..b181f2c8c1 100644
--- a/src/config/types.ts
+++ b/src/config/types.ts
@@ -41,10 +41,6 @@ export type Source = {
 		secret_key: string;
 	};
 	accesslog?: string;
-	accesses?: {
-		enable: boolean;
-		port: number;
-	};
 	twitter?: {
 		consumer_key: string;
 		consumer_secret: string;
diff --git a/src/server/index.ts b/src/server/index.ts
index 9358cce2ac..962d3b5f4f 100644
--- a/src/server/index.ts
+++ b/src/server/index.ts
@@ -7,7 +7,6 @@ import * as http from 'http';
 import * as https from 'https';
 import * as express from 'express';
 import * as morgan from 'morgan';
-import Accesses from 'accesses';
 
 import activityPub from './activitypub';
 import webFinger from './webfinger';
@@ -21,16 +20,6 @@ const app = express();
 app.disable('x-powered-by');
 app.set('trust proxy', 'loopback');
 
-// Log
-if (config.accesses && config.accesses.enable) {
-	const accesses = new Accesses({
-		appName: 'Misskey',
-		port: config.accesses.port
-	});
-
-	app.use(accesses.express);
-}
-
 app.use(morgan(process.env.NODE_ENV == 'production' ? 'combined' : 'dev', {
 	// create a write stream (in append mode)
 	stream: config.accesslog ? fs.createWriteStream(config.accesslog) : null

From cca9963a9ab17fa3f10dab845ea774c72101d1ba Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 12 Apr 2018 06:18:55 +0900
Subject: [PATCH 12/34] :v:

---
 src/client/app/common/mios.ts                        |  6 ++++--
 src/client/app/desktop/views/components/settings.vue | 10 ++++++++++
 2 files changed, 14 insertions(+), 2 deletions(-)

diff --git a/src/client/app/common/mios.ts b/src/client/app/common/mios.ts
index 5e0c7d2f3b..a09af799be 100644
--- a/src/client/app/common/mios.ts
+++ b/src/client/app/common/mios.ts
@@ -444,7 +444,7 @@ export default class MiOS extends EventEmitter {
 		// Append a credential
 		if (this.isSignedIn) (data as any).i = this.i.token;
 
-		const viaStream = localStorage.getItem('enableExperimental') == 'true';
+		const viaStream = localStorage.getItem('apiViaStream') ? localStorage.getItem('apiViaStream') == 'true' : true;
 
 		return new Promise((resolve, reject) => {
 			if (viaStream) {
@@ -452,6 +452,8 @@ export default class MiOS extends EventEmitter {
 				const id = Math.random().toString();
 
 				stream.once(`api-res:${id}`, res => {
+					if (--pending === 0) spinner.parentNode.removeChild(spinner);
+
 					if (res.res) {
 						resolve(res.res);
 					} else {
@@ -503,7 +505,7 @@ export default class MiOS extends EventEmitter {
 						reject(body.error);
 					}
 				}).catch(reject);
-			/*}*/
+			}
 		});
 	}
 
diff --git a/src/client/app/desktop/views/components/settings.vue b/src/client/app/desktop/views/components/settings.vue
index 4184ae82c7..2b5aa30246 100644
--- a/src/client/app/desktop/views/components/settings.vue
+++ b/src/client/app/desktop/views/components/settings.vue
@@ -26,6 +26,12 @@
 			<mk-switch v-model="autoPopout" text="ウィンドウの自動ポップアウト">
 				<span>ウィンドウが開かれるとき、ポップアウト(ブラウザ外に切り離す)可能なら自動でポップアウトします。この設定はブラウザに記憶されます。</span>
 			</mk-switch>
+			<details>
+				<summary>詳細設定</summary>
+				<mk-switch v-model="apiViaStream" text="ストリームを経由したAPIリクエスト">
+					<span>この設定をオンにすると、websocket接続を経由してAPIリクエストが行われます(パフォーマンス向上が期待できます)。オフにすると、ネイティブの fetch APIが利用されます。この設定はこのデバイスのみ有効です。</span>
+				</mk-switch>
+			</details>
 		</section>
 
 		<section class="web" v-show="page == 'web'">
@@ -223,6 +229,7 @@ export default Vue.extend({
 			checkingForUpdate: false,
 			enableSounds: localStorage.getItem('enableSounds') == 'true',
 			autoPopout: localStorage.getItem('autoPopout') == 'true',
+			apiViaStream: localStorage.getItem('apiViaStream') ? localStorage.getItem('apiViaStream') == 'true' : true,
 			soundVolume: localStorage.getItem('soundVolume') ? parseInt(localStorage.getItem('soundVolume'), 10) : 100,
 			lang: localStorage.getItem('lang') || '',
 			preventUpdate: localStorage.getItem('preventUpdate') == 'true',
@@ -240,6 +247,9 @@ export default Vue.extend({
 		autoPopout() {
 			localStorage.setItem('autoPopout', this.autoPopout ? 'true' : 'false');
 		},
+		apiViaStream() {
+			localStorage.setItem('apiViaStream', this.apiViaStream ? 'true' : 'false');
+		},
 		enableSounds() {
 			localStorage.setItem('enableSounds', this.enableSounds ? 'true' : 'false');
 		},

From 3ffae40085da560c894e0951ec848c412816fcb7 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 12 Apr 2018 06:20:32 +0900
Subject: [PATCH 13/34] =?UTF-8?q?=E3=82=B5=E3=82=A6=E3=83=B3=E3=83=89?=
 =?UTF-8?q?=E3=81=AE=E3=83=9C=E3=83=AA=E3=83=A5=E3=83=BC=E3=83=A0=E3=81=AF?=
 =?UTF-8?q?=E3=83=87=E3=83=95=E3=82=A9=E3=83=AB=E3=83=88=E3=81=A750%?=
 =?UTF-8?q?=E3=81=AB?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 src/client/app/common/views/components/messaging-room.vue | 2 +-
 src/client/app/common/views/components/othello.game.vue   | 4 ++--
 src/client/app/desktop/views/components/settings.vue      | 4 ++--
 src/client/app/desktop/views/components/timeline.vue      | 2 +-
 4 files changed, 6 insertions(+), 6 deletions(-)

diff --git a/src/client/app/common/views/components/messaging-room.vue b/src/client/app/common/views/components/messaging-room.vue
index d30c64d74a..e1b775c33a 100644
--- a/src/client/app/common/views/components/messaging-room.vue
+++ b/src/client/app/common/views/components/messaging-room.vue
@@ -151,7 +151,7 @@ export default Vue.extend({
 			// サウンドを再生する
 			if ((this as any).os.isEnableSounds) {
 				const sound = new Audio(`${url}/assets/message.mp3`);
-				sound.volume = localStorage.getItem('soundVolume') ? parseInt(localStorage.getItem('soundVolume'), 10) / 100 : 1;
+				sound.volume = localStorage.getItem('soundVolume') ? parseInt(localStorage.getItem('soundVolume'), 10) / 100 : 0.5;
 				sound.play();
 			}
 
diff --git a/src/client/app/common/views/components/othello.game.vue b/src/client/app/common/views/components/othello.game.vue
index b9d946de96..8c646cce07 100644
--- a/src/client/app/common/views/components/othello.game.vue
+++ b/src/client/app/common/views/components/othello.game.vue
@@ -164,7 +164,7 @@ export default Vue.extend({
 			// サウンドを再生する
 			if ((this as any).os.isEnableSounds) {
 				const sound = new Audio(`${url}/assets/othello-put-me.mp3`);
-				sound.volume = localStorage.getItem('soundVolume') ? parseInt(localStorage.getItem('soundVolume'), 10) / 100 : 1;
+				sound.volume = localStorage.getItem('soundVolume') ? parseInt(localStorage.getItem('soundVolume'), 10) / 100 : 0.5;
 				sound.play();
 			}
 
@@ -188,7 +188,7 @@ export default Vue.extend({
 			// サウンドを再生する
 			if ((this as any).os.isEnableSounds && x.color != this.myColor) {
 				const sound = new Audio(`${url}/assets/othello-put-you.mp3`);
-				sound.volume = localStorage.getItem('soundVolume') ? parseInt(localStorage.getItem('soundVolume'), 10) / 100 : 1;
+				sound.volume = localStorage.getItem('soundVolume') ? parseInt(localStorage.getItem('soundVolume'), 10) / 100 : 0.5;
 				sound.play();
 			}
 		},
diff --git a/src/client/app/desktop/views/components/settings.vue b/src/client/app/desktop/views/components/settings.vue
index 2b5aa30246..9d074165e5 100644
--- a/src/client/app/desktop/views/components/settings.vue
+++ b/src/client/app/desktop/views/components/settings.vue
@@ -230,7 +230,7 @@ export default Vue.extend({
 			enableSounds: localStorage.getItem('enableSounds') == 'true',
 			autoPopout: localStorage.getItem('autoPopout') == 'true',
 			apiViaStream: localStorage.getItem('apiViaStream') ? localStorage.getItem('apiViaStream') == 'true' : true,
-			soundVolume: localStorage.getItem('soundVolume') ? parseInt(localStorage.getItem('soundVolume'), 10) : 100,
+			soundVolume: localStorage.getItem('soundVolume') ? parseInt(localStorage.getItem('soundVolume'), 10) : 50,
 			lang: localStorage.getItem('lang') || '',
 			preventUpdate: localStorage.getItem('preventUpdate') == 'true',
 			debug: localStorage.getItem('debug') == 'true',
@@ -347,7 +347,7 @@ export default Vue.extend({
 		},
 		soundTest() {
 			const sound = new Audio(`${url}/assets/message.mp3`);
-			sound.volume = localStorage.getItem('soundVolume') ? parseInt(localStorage.getItem('soundVolume'), 10) / 100 : 1;
+			sound.volume = localStorage.getItem('soundVolume') ? parseInt(localStorage.getItem('soundVolume'), 10) / 100 : 0.5;
 			sound.play();
 		}
 	}
diff --git a/src/client/app/desktop/views/components/timeline.vue b/src/client/app/desktop/views/components/timeline.vue
index e1f88b62f3..f148e840ad 100644
--- a/src/client/app/desktop/views/components/timeline.vue
+++ b/src/client/app/desktop/views/components/timeline.vue
@@ -97,7 +97,7 @@ export default Vue.extend({
 			// サウンドを再生する
 			if ((this as any).os.isEnableSounds) {
 				const sound = new Audio(`${url}/assets/post.mp3`);
-				sound.volume = localStorage.getItem('soundVolume') ? parseInt(localStorage.getItem('soundVolume'), 10) / 100 : 1;
+				sound.volume = localStorage.getItem('soundVolume') ? parseInt(localStorage.getItem('soundVolume'), 10) / 100 : 0.5;
 				sound.play();
 			}
 

From a5ab80bf02330db30da7707409164edbb77c245e Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 12 Apr 2018 06:20:53 +0900
Subject: [PATCH 14/34] oops

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

diff --git a/src/models/drive-file.ts b/src/models/drive-file.ts
index ff31ba05dd..80fe8e0300 100644
--- a/src/models/drive-file.ts
+++ b/src/models/drive-file.ts
@@ -114,13 +114,13 @@ export const pack = (
 	let _file: any;
 
 	// Populate the file if 'file' is ID
-	if (mongodb.ObjectID.prototype.isPrototypeOf(file)) {
+	if (mongo.ObjectID.prototype.isPrototypeOf(file)) {
 		_file = await DriveFile.findOne({
 			_id: file
 		});
 	} else if (typeof file === 'string') {
 		_file = await DriveFile.findOne({
-			_id: new mongodb.ObjectID(file)
+			_id: new mongo.ObjectID(file)
 		});
 	} else {
 		_file = deepcopy(file);

From a015524cb5874dd33c884588dd2e35faa63ca08f 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, 12 Apr 2018 07:13:15 +0900
Subject: [PATCH 15/34] wip

---
 src/models/following.ts | 27 +++++++++++++++++++++++++++
 src/models/user.ts      |  8 +++++++-
 2 files changed, 34 insertions(+), 1 deletion(-)

diff --git a/src/models/following.ts b/src/models/following.ts
index b4090d8c7e..f10e349ee9 100644
--- a/src/models/following.ts
+++ b/src/models/following.ts
@@ -11,3 +11,30 @@ export type IFollowing = {
 	followeeId: mongo.ObjectID;
 	followerId: mongo.ObjectID;
 };
+
+/**
+ * Followingを物理削除します
+ */
+export async function deleteFollowing(following: string | mongo.ObjectID | IFollowing) {
+	let f: IFollowing;
+
+	// Populate
+	if (mongo.ObjectID.prototype.isPrototypeOf(following)) {
+		f = await Following.findOne({
+			_id: following
+		});
+	} else if (typeof following === 'string') {
+		f = await Following.findOne({
+			_id: new mongo.ObjectID(following)
+		});
+	} else {
+		f = following as IFollowing;
+	}
+
+	if (f == null) return;
+
+	// このFollowingを削除
+	await Following.remove({
+		_id: f._id
+	});
+}
diff --git a/src/models/user.ts b/src/models/user.ts
index ff1c11e76c..cbc445256b 100644
--- a/src/models/user.ts
+++ b/src/models/user.ts
@@ -3,7 +3,7 @@ import deepcopy = require('deepcopy');
 import rap from '@prezzemolo/rap';
 import db from '../db/mongodb';
 import Note, { INote, pack as packNote, deleteNote } from './note';
-import Following from './following';
+import Following, { deleteFollowing } from './following';
 import Mute, { deleteMute } from './mute';
 import getFriends from '../server/api/common/get-friends';
 import config from '../config';
@@ -212,8 +212,14 @@ export async function deleteUser(user: string | mongo.ObjectID | IUser) {
 	).map(x => deleteMute(x)));
 
 	// このユーザーのFollowingをすべて削除
+	await Promise.all((
+		await Following.find({ followerId: u._id })
+	).map(x => deleteFollowing(x)));
 
 	// このユーザーへのFollowingをすべて削除
+	await Promise.all((
+		await Following.find({ followeeId: u._id })
+	).map(x => deleteFollowing(x)));
 
 	// このユーザーのFollowingLogをすべて削除
 

From 991635f9190976433f9e03cef76eb36454145a19 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, 12 Apr 2018 07:19:28 +0900
Subject: [PATCH 16/34] wip

---
 src/models/note.ts      |  8 +++++++-
 src/models/poll-vote.ts | 27 +++++++++++++++++++++++++++
 src/models/user.ts      |  6 ++++++
 3 files changed, 40 insertions(+), 1 deletion(-)

diff --git a/src/models/note.ts b/src/models/note.ts
index 6e7b6cee79..3c1c2e18e1 100644
--- a/src/models/note.ts
+++ b/src/models/note.ts
@@ -5,12 +5,13 @@ 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 Vote, { deletePollVote } from './poll-vote';
 import Reaction, { deleteNoteReaction } from './note-reaction';
 import { pack as packFile } from './drive-file';
 import NoteWatching, { deleteNoteWatching } from './note-watching';
 import NoteReaction from './note-reaction';
 import Favorite, { deleteFavorite } from './favorite';
+import PollVote from './poll-vote';
 
 const Note = db.get<INote>('notes');
 
@@ -113,6 +114,11 @@ export async function deleteNote(note: string | mongo.ObjectID | INote) {
 		await NoteReaction.find({ noteId: n._id })
 	).map(x => deleteNoteReaction(x)));
 
+	// この投稿に対するPollVoteをすべて削除
+	await Promise.all((
+		await PollVote.find({ noteId: n._id })
+	).map(x => deletePollVote(x)));
+
 	// この投稿に対するFavoriteをすべて削除
 	await Promise.all((
 		await Favorite.find({ noteId: n._id })
diff --git a/src/models/poll-vote.ts b/src/models/poll-vote.ts
index 4d33b100e7..85c8454ddc 100644
--- a/src/models/poll-vote.ts
+++ b/src/models/poll-vote.ts
@@ -11,3 +11,30 @@ export interface IPollVote {
 	noteId: mongo.ObjectID;
 	choice: number;
 }
+
+/**
+ * PollVoteを物理削除します
+ */
+export async function deletePollVote(pollVote: string | mongo.ObjectID | IPollVote) {
+	let p: IPollVote;
+
+	// Populate
+	if (mongo.ObjectID.prototype.isPrototypeOf(pollVote)) {
+		p = await PollVote.findOne({
+			_id: pollVote
+		});
+	} else if (typeof pollVote === 'string') {
+		p = await PollVote.findOne({
+			_id: new mongo.ObjectID(pollVote)
+		});
+	} else {
+		p = pollVote as IPollVote;
+	}
+
+	if (p == null) return;
+
+	// このPollVoteを削除
+	await PollVote.remove({
+		_id: p._id
+	});
+}
diff --git a/src/models/user.ts b/src/models/user.ts
index cbc445256b..d7249c944d 100644
--- a/src/models/user.ts
+++ b/src/models/user.ts
@@ -15,6 +15,7 @@ import MessagingMessage, { deleteMessagingMessage } from './messaging-message';
 import MessagingHistory, { deleteMessagingHistory } from './messaging-history';
 import DriveFile, { deleteDriveFile } from './drive-file';
 import DriveFolder, { deleteDriveFolder } from './drive-folder';
+import PollVote, { deletePollVote } from './poll-vote';
 
 const User = db.get<IUser>('users');
 
@@ -171,6 +172,11 @@ export async function deleteUser(user: string | mongo.ObjectID | IUser) {
 		await NoteWatching.find({ userId: u._id })
 	).map(x => deleteNoteWatching(x)));
 
+	// このユーザーのPollVoteをすべて削除
+	await Promise.all((
+		await PollVote.find({ userId: u._id })
+	).map(x => deletePollVote(x)));
+
 	// このユーザーのFavoriteをすべて削除
 	await Promise.all((
 		await Favorite.find({ userId: u._id })

From 7b05e01819e214f8eadc64e8cca259f0af95157a 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, 12 Apr 2018 07:25:46 +0900
Subject: [PATCH 17/34] wip

---
 src/models/followed-log.ts  | 33 ++++++++++++++++++++++++++++++---
 src/models/following-log.ts | 33 ++++++++++++++++++++++++++++++---
 src/models/user.ts          |  8 ++++++++
 3 files changed, 68 insertions(+), 6 deletions(-)

diff --git a/src/models/followed-log.ts b/src/models/followed-log.ts
index 9e3ca17822..7d488b9cd3 100644
--- a/src/models/followed-log.ts
+++ b/src/models/followed-log.ts
@@ -1,12 +1,39 @@
-import { ObjectID } from 'mongodb';
+import * as mongo from 'mongodb';
 import db from '../db/mongodb';
 
 const FollowedLog = db.get<IFollowedLog>('followedLogs');
 export default FollowedLog;
 
 export type IFollowedLog = {
-	_id: ObjectID;
+	_id: mongo.ObjectID;
 	createdAt: Date;
-	userId: ObjectID;
+	userId: mongo.ObjectID;
 	count: number;
 };
+
+/**
+ * FollowedLogを物理削除します
+ */
+export async function deleteFollowedLog(followedLog: string | mongo.ObjectID | IFollowedLog) {
+	let f: IFollowedLog;
+
+	// Populate
+	if (mongo.ObjectID.prototype.isPrototypeOf(followedLog)) {
+		f = await FollowedLog.findOne({
+			_id: followedLog
+		});
+	} else if (typeof followedLog === 'string') {
+		f = await FollowedLog.findOne({
+			_id: new mongo.ObjectID(followedLog)
+		});
+	} else {
+		f = followedLog as IFollowedLog;
+	}
+
+	if (f == null) return;
+
+	// このFollowedLogを削除
+	await FollowedLog.remove({
+		_id: f._id
+	});
+}
diff --git a/src/models/following-log.ts b/src/models/following-log.ts
index 045ff7bf02..c06a337fd4 100644
--- a/src/models/following-log.ts
+++ b/src/models/following-log.ts
@@ -1,12 +1,39 @@
-import { ObjectID } from 'mongodb';
+import * as mongo from 'mongodb';
 import db from '../db/mongodb';
 
 const FollowingLog = db.get<IFollowingLog>('followingLogs');
 export default FollowingLog;
 
 export type IFollowingLog = {
-	_id: ObjectID;
+	_id: mongo.ObjectID;
 	createdAt: Date;
-	userId: ObjectID;
+	userId: mongo.ObjectID;
 	count: number;
 };
+
+/**
+ * FollowingLogを物理削除します
+ */
+export async function deleteFollowingLog(followingLog: string | mongo.ObjectID | IFollowingLog) {
+	let f: IFollowingLog;
+
+	// Populate
+	if (mongo.ObjectID.prototype.isPrototypeOf(followingLog)) {
+		f = await FollowingLog.findOne({
+			_id: followingLog
+		});
+	} else if (typeof followingLog === 'string') {
+		f = await FollowingLog.findOne({
+			_id: new mongo.ObjectID(followingLog)
+		});
+	} else {
+		f = followingLog as IFollowingLog;
+	}
+
+	if (f == null) return;
+
+	// このFollowingLogを削除
+	await FollowingLog.remove({
+		_id: f._id
+	});
+}
diff --git a/src/models/user.ts b/src/models/user.ts
index d7249c944d..a4b7becbd2 100644
--- a/src/models/user.ts
+++ b/src/models/user.ts
@@ -16,6 +16,8 @@ import MessagingHistory, { deleteMessagingHistory } from './messaging-history';
 import DriveFile, { deleteDriveFile } from './drive-file';
 import DriveFolder, { deleteDriveFolder } from './drive-folder';
 import PollVote, { deletePollVote } from './poll-vote';
+import FollowingLog, { deleteFollowingLog } from './following-log';
+import FollowedLog, { deleteFollowedLog } from './followed-log';
 
 const User = db.get<IUser>('users');
 
@@ -228,8 +230,14 @@ export async function deleteUser(user: string | mongo.ObjectID | IUser) {
 	).map(x => deleteFollowing(x)));
 
 	// このユーザーのFollowingLogをすべて削除
+	await Promise.all((
+		await FollowingLog.find({ userId: u._id })
+	).map(x => deleteFollowingLog(x)));
 
 	// このユーザーのFollowedLogをすべて削除
+	await Promise.all((
+		await FollowedLog.find({ userId: u._id })
+	).map(x => deleteFollowedLog(x)));
 
 	// このユーザーを削除
 }

From 051ab451816edfb71f54d8aa887a13633cdda187 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, 12 Apr 2018 07:32:35 +0900
Subject: [PATCH 18/34] wip

---
 src/models/sw-subscription.ts | 28 ++++++++++++++++++++++++++++
 src/models/user.ts            |  6 ++++++
 2 files changed, 34 insertions(+)

diff --git a/src/models/sw-subscription.ts b/src/models/sw-subscription.ts
index 743d0d2dd9..621ac8a9b6 100644
--- a/src/models/sw-subscription.ts
+++ b/src/models/sw-subscription.ts
@@ -11,3 +11,31 @@ export interface ISwSubscription {
 	auth: string;
 	publickey: string;
 }
+
+/**
+ * SwSubscriptionを物理削除します
+ */
+export async function deleteSwSubscription(swSubscription: string | mongo.ObjectID | ISwSubscription) {
+	let s: ISwSubscription;
+
+	// Populate
+	if (mongo.ObjectID.prototype.isPrototypeOf(swSubscription)) {
+		s = await SwSubscription.findOne({
+			_id: swSubscription
+		});
+	} else if (typeof swSubscription === 'string') {
+		s = await SwSubscription.findOne({
+			_id: new mongo.ObjectID(swSubscription)
+		});
+	} else {
+		s = swSubscription as ISwSubscription;
+	}
+
+	if (s == null) return;
+
+	// このSwSubscriptionを削除
+	await SwSubscription.remove({
+		_id: s._id
+	});
+}
+
diff --git a/src/models/user.ts b/src/models/user.ts
index a4b7becbd2..c121790c31 100644
--- a/src/models/user.ts
+++ b/src/models/user.ts
@@ -18,6 +18,7 @@ import DriveFolder, { deleteDriveFolder } from './drive-folder';
 import PollVote, { deletePollVote } from './poll-vote';
 import FollowingLog, { deleteFollowingLog } from './following-log';
 import FollowedLog, { deleteFollowedLog } from './followed-log';
+import SwSubscription, { deleteSwSubscription } from './sw-subscription';
 
 const User = db.get<IUser>('users');
 
@@ -239,6 +240,11 @@ export async function deleteUser(user: string | mongo.ObjectID | IUser) {
 		await FollowedLog.find({ userId: u._id })
 	).map(x => deleteFollowedLog(x)));
 
+	// このユーザーのSwSubscriptionをすべて削除
+	await Promise.all((
+		await SwSubscription.find({ userId: u._id })
+	).map(x => deleteSwSubscription(x)));
+
 	// このユーザーを削除
 }
 

From ec3fdbc6db1f513d3abcc5df730dc1bacfc52c45 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 13 Apr 2018 00:07:13 +0900
Subject: [PATCH 19/34] Add home customize link

---
 src/client/app/desktop/views/components/ui.header.account.vue | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/src/client/app/desktop/views/components/ui.header.account.vue b/src/client/app/desktop/views/components/ui.header.account.vue
index ec4635f338..61c3019294 100644
--- a/src/client/app/desktop/views/components/ui.header.account.vue
+++ b/src/client/app/desktop/views/components/ui.header.account.vue
@@ -18,6 +18,9 @@
 				</li>
 			</ul>
 			<ul>
+				<li>
+					<a href="/i/customize-home">%fa:wrench%カスタマイズ%fa:angle-right%</a>
+				</li>
 				<li @click="settings">
 					<p>%fa:cog%%i18n:desktop.tags.mk-ui-header-account.settings%%fa:angle-right%</p>
 				</li>

From a3bd4ba42693b1dc99ef586ef35f61dc53cdf9e9 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 13 Apr 2018 00:51:55 +0900
Subject: [PATCH 20/34] wip

---
 package.json                        |   5 +
 src/server/activitypub.ts           | 142 ++++++++++++++++++++++++++++
 src/server/activitypub/inbox.ts     |  32 -------
 src/server/activitypub/index.ts     |  18 ----
 src/server/activitypub/note.ts      |  28 ------
 src/server/activitypub/outbox.ts    |  28 ------
 src/server/activitypub/publickey.ts |  23 -----
 src/server/activitypub/user.ts      |  19 ----
 src/server/index.ts                 |  69 ++++----------
 src/server/log-request.ts           |  21 ----
 src/server/webfinger.ts             |  32 ++++---
 11 files changed, 188 insertions(+), 229 deletions(-)
 create mode 100644 src/server/activitypub.ts
 delete mode 100644 src/server/activitypub/inbox.ts
 delete mode 100644 src/server/activitypub/index.ts
 delete mode 100644 src/server/activitypub/note.ts
 delete mode 100644 src/server/activitypub/outbox.ts
 delete mode 100644 src/server/activitypub/publickey.ts
 delete mode 100644 src/server/activitypub/user.ts
 delete mode 100644 src/server/log-request.ts

diff --git a/package.json b/package.json
index d5233d54a6..e5180fddbd 100644
--- a/package.json
+++ b/package.json
@@ -56,6 +56,9 @@
 		"@types/is-root": "1.0.0",
 		"@types/is-url": "1.2.28",
 		"@types/js-yaml": "3.11.1",
+		"@types/koa": "^2.0.45",
+		"@types/koa-bodyparser": "^4.2.0",
+		"@types/koa-router": "^7.0.27",
 		"@types/kue": "^0.11.8",
 		"@types/license-checker": "15.0.0",
 		"@types/mkdirp": "0.5.2",
@@ -140,6 +143,8 @@
 		"is-url": "1.2.4",
 		"js-yaml": "3.11.0",
 		"jsdom": "11.7.0",
+		"koa": "^2.5.0",
+		"koa-router": "^7.4.0",
 		"kue": "0.11.6",
 		"license-checker": "18.0.0",
 		"loader-utils": "1.1.0",
diff --git a/src/server/activitypub.ts b/src/server/activitypub.ts
new file mode 100644
index 0000000000..ed0311af9d
--- /dev/null
+++ b/src/server/activitypub.ts
@@ -0,0 +1,142 @@
+import * as Router from 'koa-router';
+import { parseRequest } from 'http-signature';
+
+import { createHttp } from '../queue';
+import context from '../remote/activitypub/renderer/context';
+import render from '../remote/activitypub/renderer/note';
+import Note from '../models/note';
+import User, { isLocalUser } from '../models/user';
+import renderNote from '../remote/activitypub/renderer/note';
+import renderKey from '../remote/activitypub/renderer/key';
+import renderPerson from '../remote/activitypub/renderer/person';
+import renderOrderedCollection from '../remote/activitypub/renderer/ordered-collection';
+//import parseAcct from '../acct/parse';
+import config from '../config';
+
+// Init router
+const router = new Router();
+
+//#region Routing
+
+// inbox
+router.post('/users/:user/inbox', ctx => {
+	let signature;
+
+	ctx.req.headers.authorization = 'Signature ' + ctx.req.headers.signature;
+
+	try {
+		signature = parseRequest(ctx.req);
+	} catch (e) {
+		ctx.status = 401;
+		return;
+	}
+
+	createHttp({
+		type: 'processInbox',
+		activity: ctx.request.body,
+		signature
+	}).save();
+
+	ctx.status = 202;
+});
+
+// note
+router.get('/notes/:note', async (ctx, next) => {
+	const accepted = ctx.accepts('html', 'application/activity+json', 'application/ld+json');
+	if (!['application/activity+json', 'application/ld+json'].includes(accepted as string)) {
+		next();
+		return;
+	}
+
+	const note = await Note.findOne({
+		_id: ctx.params.note
+	});
+
+	if (note === null) {
+		ctx.status = 404;
+		return;
+	}
+
+	const rendered = await render(note);
+	rendered['@context'] = context;
+
+	ctx.body = rendered;
+});
+
+// outbot
+router.get('/users/:user/outbox', async ctx => {
+	const userId = ctx.params.user;
+
+	const user = await User.findOne({ _id: userId });
+
+	if (user === null) {
+		ctx.status = 404;
+		return;
+	}
+
+	const notes = await Note.find({ userId: user._id }, {
+		limit: 10,
+		sort: { _id: -1 }
+	});
+
+	const renderedNotes = await Promise.all(notes.map(note => renderNote(note)));
+	const rendered = renderOrderedCollection(`${config.url}/users/${userId}/inbox`, user.notesCount, renderedNotes);
+	rendered['@context'] = context;
+
+	ctx.body = rendered;
+});
+
+// publickey
+router.get('/users/:user/publickey', async ctx => {
+	const userId = ctx.params.user;
+
+	const user = await User.findOne({ _id: userId });
+
+	if (user === null) {
+		ctx.status = 404;
+		return;
+	}
+
+	if (isLocalUser(user)) {
+		const rendered = renderKey(user);
+		rendered['@context'] = context;
+
+		ctx.body = rendered;
+	} else {
+		ctx.status = 400;
+	}
+});
+
+// user
+router.get('/users/:user', async ctx => {
+	const userId = ctx.params.user;
+
+	const user = await User.findOne({ _id: userId });
+
+	if (user === null) {
+		ctx.status = 404;
+		return;
+	}
+
+	const rendered = renderPerson(user);
+	rendered['@context'] = context;
+
+	ctx.body = rendered;
+});
+
+// follow form
+router.get('/authorize-follow', async ctx => {
+	/* TODO
+	const { username, host } = parseAcct(ctx.query.acct);
+	if (host === null) {
+		res.sendStatus(422);
+		return;
+	}
+
+	const finger = await request(`https://${host}`)
+	*/
+});
+
+//#endregion
+
+export default router;
diff --git a/src/server/activitypub/inbox.ts b/src/server/activitypub/inbox.ts
deleted file mode 100644
index 643d2945bd..0000000000
--- a/src/server/activitypub/inbox.ts
+++ /dev/null
@@ -1,32 +0,0 @@
-import * as bodyParser from 'body-parser';
-import * as express from 'express';
-import { parseRequest } from 'http-signature';
-import { createHttp } from '../../queue';
-
-const app = express.Router();
-
-app.post('/users/:user/inbox', bodyParser.json({
-	type() {
-		return true;
-	}
-}), async (req, res) => {
-	let signature;
-
-	req.headers.authorization = 'Signature ' + req.headers.signature;
-
-	try {
-		signature = parseRequest(req);
-	} catch (exception) {
-		return res.sendStatus(401);
-	}
-
-	createHttp({
-		type: 'processInbox',
-		activity: req.body,
-		signature,
-	}).save();
-
-	return res.status(202).end();
-});
-
-export default app;
diff --git a/src/server/activitypub/index.ts b/src/server/activitypub/index.ts
deleted file mode 100644
index 042579db9d..0000000000
--- a/src/server/activitypub/index.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-import * as express from 'express';
-
-import user from './user';
-import inbox from './inbox';
-import outbox from './outbox';
-import publicKey from './publickey';
-import note from './note';
-
-const app = express();
-app.disable('x-powered-by');
-
-app.use(user);
-app.use(inbox);
-app.use(outbox);
-app.use(publicKey);
-app.use(note);
-
-export default app;
diff --git a/src/server/activitypub/note.ts b/src/server/activitypub/note.ts
deleted file mode 100644
index 1c2e695b80..0000000000
--- a/src/server/activitypub/note.ts
+++ /dev/null
@@ -1,28 +0,0 @@
-import * as express from 'express';
-import context from '../../remote/activitypub/renderer/context';
-import render from '../../remote/activitypub/renderer/note';
-import Note from '../../models/note';
-
-const app = express.Router();
-
-app.get('/notes/:note', async (req, res, next) => {
-	const accepted = req.accepts(['html', 'application/activity+json', 'application/ld+json']);
-	if (!(['application/activity+json', 'application/ld+json'] as any[]).includes(accepted)) {
-		return next();
-	}
-
-	const note = await Note.findOne({
-		_id: req.params.note
-	});
-
-	if (note === null) {
-		return res.sendStatus(404);
-	}
-
-	const rendered = await render(note);
-	rendered['@context'] = context;
-
-	res.json(rendered);
-});
-
-export default app;
diff --git a/src/server/activitypub/outbox.ts b/src/server/activitypub/outbox.ts
deleted file mode 100644
index 1c97c17a2e..0000000000
--- a/src/server/activitypub/outbox.ts
+++ /dev/null
@@ -1,28 +0,0 @@
-import * as express from 'express';
-import context from '../../remote/activitypub/renderer/context';
-import renderNote from '../../remote/activitypub/renderer/note';
-import renderOrderedCollection from '../../remote/activitypub/renderer/ordered-collection';
-import config from '../../config';
-import Note from '../../models/note';
-import User from '../../models/user';
-
-const app = express.Router();
-
-app.get('/users/:user/outbox', async (req, res) => {
-	const userId = req.params.user;
-
-	const user = await User.findOne({ _id: userId });
-
-	const notes = await Note.find({ userId: user._id }, {
-		limit: 20,
-		sort: { _id: -1 }
-	});
-
-	const renderedNotes = await Promise.all(notes.map(note => renderNote(note)));
-	const rendered = renderOrderedCollection(`${config.url}/users/${userId}/inbox`, user.notesCount, renderedNotes);
-	rendered['@context'] = context;
-
-	res.json(rendered);
-});
-
-export default app;
diff --git a/src/server/activitypub/publickey.ts b/src/server/activitypub/publickey.ts
deleted file mode 100644
index e874b82729..0000000000
--- a/src/server/activitypub/publickey.ts
+++ /dev/null
@@ -1,23 +0,0 @@
-import * as express from 'express';
-import context from '../../remote/activitypub/renderer/context';
-import render from '../../remote/activitypub/renderer/key';
-import User, { isLocalUser } from '../../models/user';
-
-const app = express.Router();
-
-app.get('/users/:user/publickey', async (req, res) => {
-	const userId = req.params.user;
-
-	const user = await User.findOne({ _id: userId });
-
-	if (isLocalUser(user)) {
-		const rendered = render(user);
-		rendered['@context'] = context;
-
-		res.json(rendered);
-	} else {
-		res.sendStatus(400);
-	}
-});
-
-export default app;
diff --git a/src/server/activitypub/user.ts b/src/server/activitypub/user.ts
deleted file mode 100644
index 9e98e92b6a..0000000000
--- a/src/server/activitypub/user.ts
+++ /dev/null
@@ -1,19 +0,0 @@
-import * as express from 'express';
-import context from '../../remote/activitypub/renderer/context';
-import render from '../../remote/activitypub/renderer/person';
-import User from '../../models/user';
-
-const app = express.Router();
-
-app.get('/users/:user', async (req, res) => {
-	const userId = req.params.user;
-
-	const user = await User.findOne({ _id: userId });
-
-	const rendered = render(user);
-	rendered['@context'] = context;
-
-	res.json(rendered);
-});
-
-export default app;
diff --git a/src/server/index.ts b/src/server/index.ts
index 962d3b5f4f..e9bfa9e10b 100644
--- a/src/server/index.ts
+++ b/src/server/index.ts
@@ -5,67 +5,40 @@
 import * as fs from 'fs';
 import * as http from 'http';
 import * as https from 'https';
-import * as express from 'express';
-import * as morgan from 'morgan';
+import * as Koa from 'koa';
+import * as Router from 'koa-router';
+import * as bodyParser from 'koa-bodyparser';
 
 import activityPub from './activitypub';
 import webFinger from './webfinger';
-import log from './log-request';
 import config from '../config';
 
-/**
- * Init app
- */
-const app = express();
-app.disable('x-powered-by');
-app.set('trust proxy', 'loopback');
+// Init server
+const app = new Koa();
+app.proxy = true;
+app.use(bodyParser);
 
-app.use(morgan(process.env.NODE_ENV == 'production' ? 'combined' : 'dev', {
-	// create a write stream (in append mode)
-	stream: config.accesslog ? fs.createWriteStream(config.accesslog) : null
-}));
-
-app.use((req, res, next) => {
-	log(req);
-	next();
-});
-
-/**
- * HSTS
- * 6month(15552000sec)
- */
+// HSTS
+// 6months (15552000sec)
 if (config.url.startsWith('https')) {
-	app.use((req, res, next) => {
-		res.header('strict-transport-security', 'max-age=15552000; preload');
+	app.use((ctx, next) => {
+		ctx.set('strict-transport-security', 'max-age=15552000; preload');
 		next();
 	});
 }
 
-// Drop request when without 'Host' header
-app.use((req, res, next) => {
-	if (!req.headers['host']) {
-		res.sendStatus(400);
-	} else {
-		next();
-	}
-});
+// Init router
+const router = new Router();
 
-// 互換性のため
-app.post('/meta', (req, res) => {
-	res.header('Access-Control-Allow-Origin', '*');
-	res.json({
-		version: 'nighthike'
-	});
-});
+// Routing
+router.use('/api', require('./api'));
+router.use('/files', require('./file'));
+router.use(activityPub.routes());
+router.use(webFinger.routes());
+router.use(require('./web'));
 
-/**
- * Register modules
- */
-app.use('/api', require('./api'));
-app.use('/files', require('./file'));
-app.use(activityPub);
-app.use(webFinger);
-app.use(require('./web'));
+// Register router
+app.use(router.routes());
 
 function createServer() {
 	if (config.https) {
diff --git a/src/server/log-request.ts b/src/server/log-request.ts
deleted file mode 100644
index e431aa271d..0000000000
--- a/src/server/log-request.ts
+++ /dev/null
@@ -1,21 +0,0 @@
-import * as crypto from 'crypto';
-import * as express from 'express';
-import * as proxyAddr from 'proxy-addr';
-import Xev from 'xev';
-
-const ev = new Xev();
-
-export default function(req: express.Request) {
-	const ip = proxyAddr(req, () => true);
-
-	const md5 = crypto.createHash('md5');
-	md5.update(ip);
-	const hashedIp = md5.digest('hex').substr(0, 3);
-
-	ev.emit('request', {
-		ip: hashedIp,
-		method: req.method,
-		hostname: req.hostname,
-		path: req.originalUrl
-	});
-}
diff --git a/src/server/webfinger.ts b/src/server/webfinger.ts
index dbf0999f3e..e72592351b 100644
--- a/src/server/webfinger.ts
+++ b/src/server/webfinger.ts
@@ -1,17 +1,19 @@
-import * as express from 'express';
+import * as Router from 'koa-router';
 
 import config from '../config';
 import parseAcct from '../acct/parse';
 import User from '../models/user';
 
-const app = express.Router();
+// Init router
+const router = new Router();
 
-app.get('/.well-known/webfinger', async (req, res) => {
-	if (typeof req.query.resource !== 'string') {
-		return res.sendStatus(400);
+router.get('/.well-known/webfinger', async ctx => {
+	if (typeof ctx.query.resource !== 'string') {
+		ctx.status = 400;
+		return;
 	}
 
-	const resourceLower = req.query.resource.toLowerCase();
+	const resourceLower = ctx.query.resource.toLowerCase();
 	const webPrefix = config.url.toLowerCase() + '/@';
 	let acctLower;
 
@@ -25,15 +27,21 @@ app.get('/.well-known/webfinger', async (req, res) => {
 
 	const parsedAcctLower = parseAcct(acctLower);
 	if (![null, config.host.toLowerCase()].includes(parsedAcctLower.host)) {
-		return res.sendStatus(422);
+		ctx.status = 422;
+		return;
 	}
 
-	const user = await User.findOne({ usernameLower: parsedAcctLower.username, host: null });
+	const user = await User.findOne({
+		usernameLower: parsedAcctLower.username,
+		host: null
+	});
+
 	if (user === null) {
-		return res.sendStatus(404);
+		ctx.status = 404;
+		return;
 	}
 
-	return res.json({
+	ctx.body = {
 		subject: `acct:${user.username}@${config.host}`,
 		links: [{
 			rel: 'self',
@@ -47,7 +55,7 @@ app.get('/.well-known/webfinger', async (req, res) => {
 			rel: 'http://ostatus.org/schema/1.0/subscribe',
 			template: `${config.url}/authorize-follow?acct={uri}`
 		}]
-	});
+	};
 });
 
-export default app;
+export default router;

From 3368fe855249f45bdf1e4c1e509d325d44e80fbe Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 13 Apr 2018 06:06:18 +0900
Subject: [PATCH 21/34] wip

---
 package.json                          |  10 +
 src/client/app/boot.js                |   2 +
 src/client/assets/404.js              |  25 ---
 src/index.ts                          |   6 -
 src/server/api/api-handler.ts         |  17 +-
 src/server/api/bot/interfaces/line.ts | 117 ++++++-----
 src/server/api/call.ts                |   7 +-
 src/server/api/common/signin.ts       |  17 +-
 src/server/api/index.ts               |  56 +++---
 src/server/api/private/signin.ts      |  36 ++--
 src/server/api/private/signup.ts      |  20 +-
 src/server/api/service/github.ts      | 266 ++++++++++++++------------
 src/server/api/service/twitter.ts     | 181 +++++++++---------
 src/server/file/index.ts              | 172 ++---------------
 src/server/file/pour.ts               |  93 +++++++++
 src/server/file/send-drive-file.ts    |  30 +++
 src/server/index.ts                   |   6 +-
 src/server/web/docs.ts                |  25 ++-
 src/server/web/index.ts               |  97 +++++-----
 src/server/web/url-preview.ts         |   8 +-
 20 files changed, 582 insertions(+), 609 deletions(-)
 delete mode 100644 src/client/assets/404.js
 create mode 100644 src/server/file/pour.ts
 create mode 100644 src/server/file/send-drive-file.ts

diff --git a/package.json b/package.json
index e5180fddbd..3e349203fc 100644
--- a/package.json
+++ b/package.json
@@ -30,6 +30,7 @@
 		"@fortawesome/fontawesome-free-brands": "5.0.2",
 		"@fortawesome/fontawesome-free-regular": "5.0.2",
 		"@fortawesome/fontawesome-free-solid": "5.0.2",
+		"@koa/cors": "^2.2.1",
 		"@prezzemolo/rap": "0.1.2",
 		"@prezzemolo/zip": "0.0.3",
 		"@types/bcryptjs": "2.4.1",
@@ -58,7 +59,12 @@
 		"@types/js-yaml": "3.11.1",
 		"@types/koa": "^2.0.45",
 		"@types/koa-bodyparser": "^4.2.0",
+		"@types/koa-favicon": "^2.0.19",
+		"@types/koa-mount": "^3.0.1",
+		"@types/koa-multer": "^1.0.0",
 		"@types/koa-router": "^7.0.27",
+		"@types/koa-send": "^4.1.1",
+		"@types/koa__cors": "^2.2.2",
 		"@types/kue": "^0.11.8",
 		"@types/license-checker": "15.0.0",
 		"@types/mkdirp": "0.5.2",
@@ -144,7 +150,11 @@
 		"js-yaml": "3.11.0",
 		"jsdom": "11.7.0",
 		"koa": "^2.5.0",
+		"koa-favicon": "^2.0.1",
+		"koa-mount": "^3.0.0",
+		"koa-multer": "^1.0.2",
 		"koa-router": "^7.4.0",
+		"koa-send": "^4.1.3",
 		"kue": "0.11.6",
 		"license-checker": "18.0.0",
 		"loader-utils": "1.1.0",
diff --git a/src/client/app/boot.js b/src/client/app/boot.js
index 0846e4bd55..ef828d9637 100644
--- a/src/client/app/boot.js
+++ b/src/client/app/boot.js
@@ -97,6 +97,8 @@
 
 		// Compare versions
 		if (meta.version != ver) {
+			localStorage.setItem('v', meta.version);
+
 			alert(
 				'Misskeyの新しいバージョンがあります。ページを再度読み込みします。' +
 				'\n\n' +
diff --git a/src/client/assets/404.js b/src/client/assets/404.js
deleted file mode 100644
index 9e498fe7c2..0000000000
--- a/src/client/assets/404.js
+++ /dev/null
@@ -1,25 +0,0 @@
-const yn = window.confirm(
-	'サーバー上に存在しないスクリプトがリクエストされました。お使いのMisskeyのバージョンが古いことが原因の可能性があります。Misskeyを更新しますか?\n\nA script that does not exist on the server was requested. It may be caused by an old version of Misskey you’re using. Do you want to delete the cache?');
-
-const langYn = window.confirm('また、言語を日本語に設定すると解決する場合があります。日本語に設定しますか?\n\nAlso, setting the language to Japanese may solve the problem. Would you like to set it to Japanese?');
-
-if (langYn) {
-	localStorage.setItem('lang', 'ja');
-}
-
-if (yn) {
-	// Clear cache (serive worker)
-	try {
-		navigator.serviceWorker.controller.postMessage('clear');
-
-		navigator.serviceWorker.getRegistrations().then(registrations => {
-			registrations.forEach(registration => registration.unregister());
-		});
-	} catch (e) {
-		console.error(e);
-	}
-
-	localStorage.removeItem('v');
-
-	location.reload(true);
-}
diff --git a/src/index.ts b/src/index.ts
index 68b289793b..d633fcbbcb 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -10,7 +10,6 @@ import * as debug from 'debug';
 import chalk from 'chalk';
 // import portUsed = require('tcp-port-used');
 import isRoot = require('is-root');
-import { master } from 'accesses';
 import Xev from 'xev';
 
 import Logger from './utils/logger';
@@ -73,11 +72,6 @@ async function masterMain(opt) {
 
 	Logger.info(chalk.green('Successfully initialized :)'));
 
-	// Init accesses
-	if (config.accesses && config.accesses.enable) {
-		master();
-	}
-
 	spawnWorkers(() => {
 		if (!opt['only-processor']) {
 			Logger.info(chalk.bold.green(
diff --git a/src/server/api/api-handler.ts b/src/server/api/api-handler.ts
index 409069b6a0..2c50234317 100644
--- a/src/server/api/api-handler.ts
+++ b/src/server/api/api-handler.ts
@@ -1,4 +1,4 @@
-import * as express from 'express';
+import * as Koa from 'koa';
 
 import { Endpoint } from './endpoints';
 import authenticate from './authenticate';
@@ -6,16 +6,17 @@ import call from './call';
 import { IUser } from '../../models/user';
 import { IApp } from '../../models/app';
 
-export default async (endpoint: Endpoint, req: express.Request, res: express.Response) => {
+export default async (endpoint: Endpoint, ctx: Koa.Context) => {
 	const reply = (x?: any, y?: any) => {
 		if (x === undefined) {
-			res.sendStatus(204);
+			ctx.status = 204;
 		} else if (typeof x === 'number') {
-			res.status(x).send({
+			ctx.status = x;
+			ctx.body = {
 				error: x === 500 ? 'INTERNAL_ERROR' : y
-			});
+			};
 		} else {
-			res.send(x);
+			ctx.body = x;
 		}
 	};
 
@@ -24,11 +25,11 @@ export default async (endpoint: Endpoint, req: express.Request, res: express.Res
 
 	// Authentication
 	try {
-		[user, app] = await authenticate(req.body['i']);
+		[user, app] = await authenticate(ctx.body['i']);
 	} catch (e) {
 		return reply(403, 'AUTHENTICATION_FAILED');
 	}
 
 	// API invoking
-	call(endpoint, user, app, req.body, req).then(reply).catch(e => reply(400, e));
+	call(endpoint, user, app, ctx.body, ctx.req).then(reply).catch(e => reply(400, e));
 };
diff --git a/src/server/api/bot/interfaces/line.ts b/src/server/api/bot/interfaces/line.ts
index be3bfe33d3..454630161a 100644
--- a/src/server/api/bot/interfaces/line.ts
+++ b/src/server/api/bot/interfaces/line.ts
@@ -1,5 +1,5 @@
 import * as EventEmitter from 'events';
-import * as express from 'express';
+import * as Router from 'koa-router';
 import * as request from 'request';
 import * as crypto from 'crypto';
 import User from '../../../../models/user';
@@ -158,82 +158,81 @@ class LineBot extends BotCore {
 	}
 }
 
-module.exports = async (app: express.Application) => {
-	if (config.line_bot == null) return;
+const handler = new EventEmitter();
 
-	const handler = new EventEmitter();
+handler.on('event', async (ev) => {
 
-	handler.on('event', async (ev) => {
+	const sourceId = ev.source.userId;
+	const sessionId = `line-bot-sessions:${sourceId}`;
 
-		const sourceId = ev.source.userId;
-		const sessionId = `line-bot-sessions:${sourceId}`;
+	const session = await redis.get(sessionId);
+	let bot: LineBot;
 
-		const session = await redis.get(sessionId);
-		let bot: LineBot;
-
-		if (session == null) {
-			const user = await User.findOne({
-				host: null,
-				'line': {
-					userId: sourceId
-				}
-			});
-
-			bot = new LineBot(user);
-
-			bot.on('signin', user => {
-				User.update(user._id, {
-					$set: {
-						'line': {
-							userId: sourceId
-						}
-					}
-				});
-			});
-
-			bot.on('signout', user => {
-				User.update(user._id, {
-					$set: {
-						'line': {
-							userId: null
-						}
-					}
-				});
-			});
-
-			redis.set(sessionId, JSON.stringify(bot.export()));
-		} else {
-			bot = LineBot.import(JSON.parse(session));
-		}
-
-		bot.on('updated', () => {
-			redis.set(sessionId, JSON.stringify(bot.export()));
+	if (session == null) {
+		const user = await User.findOne({
+			host: null,
+			'line': {
+				userId: sourceId
+			}
 		});
 
-		if (session != null) bot.refreshUser();
+		bot = new LineBot(user);
 
-		bot.react(ev);
+		bot.on('signin', user => {
+			User.update(user._id, {
+				$set: {
+					'line': {
+						userId: sourceId
+					}
+				}
+			});
+		});
+
+		bot.on('signout', user => {
+			User.update(user._id, {
+				$set: {
+					'line': {
+						userId: null
+					}
+				}
+			});
+		});
+
+		redis.set(sessionId, JSON.stringify(bot.export()));
+	} else {
+		bot = LineBot.import(JSON.parse(session));
+	}
+
+	bot.on('updated', () => {
+		redis.set(sessionId, JSON.stringify(bot.export()));
 	});
 
-	app.post('/hooks/line', (req, res, next) => {
-		// req.headers['x-line-signature'] は常に string ですが、型定義の都合上
-		// string | string[] になっているので string を明示しています
-		const sig1 = req.headers['x-line-signature'] as string;
+	if (session != null) bot.refreshUser();
+
+	bot.react(ev);
+});
+
+// Init router
+const router = new Router();
+
+if (config.line_bot) {
+	router.post('/hooks/line', ctx => {
+		const sig1 = ctx.headers['x-line-signature'];
 
 		const hash = crypto.createHmac('SHA256', config.line_bot.channel_secret)
-			.update((req as any).rawBody);
+			.update(ctx.request.rawBody);
 
 		const sig2 = hash.digest('base64');
 
 		// シグネチャ比較
 		if (sig1 === sig2) {
-			req.body.events.forEach(ev => {
+			ctx.body.events.forEach(ev => {
 				handler.emit('event', ev);
 			});
-
-			res.sendStatus(200);
 		} else {
-			res.sendStatus(400);
+			ctx.status = 400;
 		}
 	});
-};
+}
+
+module.exports = router;
diff --git a/src/server/api/call.ts b/src/server/api/call.ts
index 1bfe94bb74..c25f55ed3f 100644
--- a/src/server/api/call.ts
+++ b/src/server/api/call.ts
@@ -1,11 +1,12 @@
-import * as express from 'express';
+import * as http from 'http';
+import * as multer from 'koa-multer';
 
 import endpoints, { Endpoint } from './endpoints';
 import limitter from './limitter';
 import { IUser } from '../../models/user';
 import { IApp } from '../../models/app';
 
-export default (endpoint: string | Endpoint, user: IUser, app: IApp, data: any, req?: express.Request) => new Promise(async (ok, rej) => {
+export default (endpoint: string | Endpoint, user: IUser, app: IApp, data: any, req?: http.IncomingMessage) => new Promise(async (ok, rej) => {
 	const isSecure = user != null && app == null;
 
 	//console.log(endpoint, user, app, data);
@@ -38,7 +39,7 @@ export default (endpoint: string | Endpoint, user: IUser, app: IApp, data: any,
 	let exec = require(`${__dirname}/endpoints/${ep.name}`);
 
 	if (ep.withFile && req) {
-		exec = exec.bind(null, req.file);
+		exec = exec.bind(null, (req as multer.MulterIncomingMessage).file);
 	}
 
 	let res;
diff --git a/src/server/api/common/signin.ts b/src/server/api/common/signin.ts
index 8bb327694d..f57c38414c 100644
--- a/src/server/api/common/signin.ts
+++ b/src/server/api/common/signin.ts
@@ -1,19 +1,20 @@
-import config from '../../../config';
+import * as Koa from 'koa';
 
-export default function(res, user, redirect: boolean) {
+import config from '../../../config';
+import { ILocalUser } from '../../../models/user';
+
+export default function(ctx: Koa.Context, user: ILocalUser, redirect: boolean) {
 	const expires = 1000 * 60 * 60 * 24 * 365; // One Year
-	res.cookie('i', user.token, {
+	ctx.cookies.set('i', user.token, {
 		path: '/',
-		domain: `.${config.hostname}`,
-		secure: config.url.substr(0, 5) === 'https',
+		domain: config.hostname,
+		secure: config.url.startsWith('https'),
 		httpOnly: false,
 		expires: new Date(Date.now() + expires),
 		maxAge: expires
 	});
 
 	if (redirect) {
-		res.redirect(config.url);
-	} else {
-		res.sendStatus(204);
+		ctx.redirect(config.url);
 	}
 }
diff --git a/src/server/api/index.ts b/src/server/api/index.ts
index 5fbacd8a0e..d2427d30ae 100644
--- a/src/server/api/index.ts
+++ b/src/server/api/index.ts
@@ -2,53 +2,41 @@
  * API Server
  */
 
-import * as express from 'express';
-import * as bodyParser from 'body-parser';
-import * as cors from 'cors';
-import * as multer from 'multer';
+import * as Koa from 'koa';
+import * as Router from 'koa-router';
+import * as multer from 'koa-multer';
 
 import endpoints from './endpoints';
 
-/**
- * Init app
- */
-const app = express();
+const handler = require('./api-handler').default;
 
-app.disable('x-powered-by');
-app.set('etag', false);
-app.use(bodyParser.urlencoded({ extended: true }));
-app.use(bodyParser.json({
-	type: ['application/json', 'text/plain'],
-	verify: (req, res, buf, encoding) => {
-		if (buf && buf.length) {
-			(req as any).rawBody = buf.toString(encoding || 'utf8');
-		}
-	}
-}));
-app.use(cors());
+// Init app
+const app = new Koa();
 
-app.get('/', (req, res) => {
-	res.send('YEE HAW');
+// Init multer instance
+const upload = multer({
+	storage: multer.diskStorage({})
 });
 
+// Init router
+const router = new Router();
+
 /**
  * Register endpoint handlers
  */
-endpoints.forEach(endpoint =>
-	endpoint.withFile ?
-		app.post(`/${endpoint.name}`,
-			endpoint.withFile ? multer({ storage: multer.diskStorage({}) }).single('file') : null,
-			require('./api-handler').default.bind(null, endpoint)) :
-		app.post(`/${endpoint.name}`,
-			require('./api-handler').default.bind(null, endpoint))
+endpoints.forEach(endpoint => endpoint.withFile
+	? router.post(`/${endpoint.name}`, upload.single('file'), handler.bind(null, endpoint))
+	: router.post(`/${endpoint.name}`, handler.bind(null, endpoint))
 );
 
-app.post('/signup', require('./private/signup').default);
-app.post('/signin', require('./private/signin').default);
+router.post('/signup', require('./private/signup').default);
+router.post('/signin', require('./private/signin').default);
 
-require('./service/github')(app);
-require('./service/twitter')(app);
+router.use(require('./service/github').routes());
+router.use(require('./service/twitter').routes());
+router.use(require('./bot/interfaces/line').routes());
 
-require('./bot/interfaces/line')(app);
+// Register router
+app.use(router.routes());
 
 module.exports = app;
diff --git a/src/server/api/private/signin.ts b/src/server/api/private/signin.ts
index 665ee21ebd..55326deeaf 100644
--- a/src/server/api/private/signin.ts
+++ b/src/server/api/private/signin.ts
@@ -1,4 +1,4 @@
-import * as express from 'express';
+import * as Koa from 'koa';
 import * as bcrypt from 'bcryptjs';
 import * as speakeasy from 'speakeasy';
 import User, { ILocalUser } from '../../../models/user';
@@ -7,26 +7,26 @@ import event from '../../../publishers/stream';
 import signin from '../common/signin';
 import config from '../../../config';
 
-export default async (req: express.Request, res: express.Response) => {
-	res.header('Access-Control-Allow-Origin', config.url);
-	res.header('Access-Control-Allow-Credentials', 'true');
+export default async (ctx: Koa.Context) => {
+	ctx.set('Access-Control-Allow-Origin', config.url);
+	ctx.set('Access-Control-Allow-Credentials', 'true');
 
-	const username = req.body['username'];
-	const password = req.body['password'];
-	const token = req.body['token'];
+	const username = ctx.body['username'];
+	const password = ctx.body['password'];
+	const token = ctx.body['token'];
 
 	if (typeof username != 'string') {
-		res.sendStatus(400);
+		ctx.status = 400;
 		return;
 	}
 
 	if (typeof password != 'string') {
-		res.sendStatus(400);
+		ctx.status = 400;
 		return;
 	}
 
 	if (token != null && typeof token != 'string') {
-		res.sendStatus(400);
+		ctx.status = 400;
 		return;
 	}
 
@@ -37,12 +37,12 @@ export default async (req: express.Request, res: express.Response) => {
 	}, {
 		fields: {
 			data: false,
-			'profile': false
+			profile: false
 		}
 	}) as ILocalUser;
 
 	if (user === null) {
-		res.status(404).send({
+		ctx.throw(404, {
 			error: 'user not found'
 		});
 		return;
@@ -60,17 +60,17 @@ export default async (req: express.Request, res: express.Response) => {
 			});
 
 			if (verified) {
-				signin(res, user, false);
+				signin(ctx, user, false);
 			} else {
-				res.status(400).send({
+				ctx.throw(400, {
 					error: 'invalid token'
 				});
 			}
 		} else {
-			signin(res, user, false);
+			signin(ctx, user, false);
 		}
 	} else {
-		res.status(400).send({
+		ctx.throw(400, {
 			error: 'incorrect password'
 		});
 	}
@@ -79,8 +79,8 @@ export default async (req: express.Request, res: express.Response) => {
 	const record = await Signin.insert({
 		createdAt: new Date(),
 		userId: user._id,
-		ip: req.ip,
-		headers: req.headers,
+		ip: ctx.ip,
+		headers: ctx.headers,
 		success: same
 	});
 
diff --git a/src/server/api/private/signup.ts b/src/server/api/private/signup.ts
index f441e1b754..a4554be4ae 100644
--- a/src/server/api/private/signup.ts
+++ b/src/server/api/private/signup.ts
@@ -1,5 +1,5 @@
 import * as uuid from 'uuid';
-import * as express from 'express';
+import * as Koa from 'koa';
 import * as bcrypt from 'bcryptjs';
 import { generate as generateKeypair } from '../../../crypto_key';
 import recaptcha = require('recaptcha-promise');
@@ -33,30 +33,30 @@ const home = {
 	]
 };
 
-export default async (req: express.Request, res: express.Response) => {
+export default async (ctx: Koa.Context) => {
 	// Verify recaptcha
 	// ただしテスト時はこの機構は障害となるため無効にする
 	if (process.env.NODE_ENV !== 'test') {
-		const success = await recaptcha(req.body['g-recaptcha-response']);
+		const success = await recaptcha(ctx.body['g-recaptcha-response']);
 
 		if (!success) {
-			res.status(400).send('recaptcha-failed');
+			ctx.throw(400, 'recaptcha-failed');
 			return;
 		}
 	}
 
-	const username = req.body['username'];
-	const password = req.body['password'];
+	const username = ctx.body['username'];
+	const password = ctx.body['password'];
 
 	// Validate username
 	if (!validateUsername(username)) {
-		res.sendStatus(400);
+		ctx.status = 400;
 		return;
 	}
 
 	// Validate password
 	if (!validatePassword(password)) {
-		res.sendStatus(400);
+		ctx.status = 400;
 		return;
 	}
 
@@ -71,7 +71,7 @@ export default async (req: express.Request, res: express.Response) => {
 
 	// Check username already used
 	if (usernameExist !== 0) {
-		res.sendStatus(400);
+		ctx.status = 400;
 		return;
 	}
 
@@ -143,5 +143,5 @@ export default async (req: express.Request, res: express.Response) => {
 	});
 
 	// Response
-	res.send(await pack(account));
+	ctx.body = await pack(account);
 };
diff --git a/src/server/api/service/github.ts b/src/server/api/service/github.ts
index bc8d3c6a7d..ee226cc5cc 100644
--- a/src/server/api/service/github.ts
+++ b/src/server/api/service/github.ts
@@ -1,140 +1,152 @@
 import * as EventEmitter from 'events';
-import * as express from 'express';
+import * as Router from 'koa-router';
 import * as request from 'request';
 const crypto = require('crypto');
 
-import User from '../../../models/user';
+import User, { IUser } from '../../../models/user';
 import createNote from '../../../services/note/create';
 import config from '../../../config';
 
-module.exports = async (app: express.Application) => {
-	if (config.github_bot == null) return;
+const handler = new EventEmitter();
 
-	const bot = await User.findOne({
-		usernameLower: config.github_bot.username.toLowerCase()
-	});
+let bot: IUser;
 
+const post = async text => {
 	if (bot == null) {
-		console.warn(`GitHub hook bot specified, but not found: @${config.github_bot.username}`);
-		return;
+		const account = await User.findOne({
+			usernameLower: config.github_bot.username.toLowerCase()
+		});
+
+		if (account == null) {
+			console.warn(`GitHub hook bot specified, but not found: @${config.github_bot.username}`);
+			return;
+		} else {
+			bot = account;
+		}
 	}
 
-	const post = text => createNote(bot, { text });
-
-	const handler = new EventEmitter();
-
-	app.post('/hooks/github', (req, res, next) => {
-		// req.headers['x-hub-signature'] および
-		// req.headers['x-github-event'] は常に string ですが、型定義の都合上
-		// string | string[] になっているので string を明示しています
-		if ((new Buffer(req.headers['x-hub-signature'] as string)).equals(new Buffer(`sha1=${crypto.createHmac('sha1', config.github_bot.hook_secret).update(JSON.stringify(req.body)).digest('hex')}`))) {
-			handler.emit(req.headers['x-github-event'] as string, req.body);
-			res.sendStatus(200);
-		} else {
-			res.sendStatus(400);
-		}
-	});
-
-	handler.on('status', event => {
-		const state = event.state;
-		switch (state) {
-			case 'error':
-			case 'failure':
-				const commit = event.commit;
-				const parent = commit.parents[0];
-
-				// Fetch parent status
-				request({
-					url: `${parent.url}/statuses`,
-					headers: {
-						'User-Agent': 'misskey'
-					}
-				}, (err, res, body) => {
-					if (err) {
-						console.error(err);
-						return;
-					}
-					const parentStatuses = JSON.parse(body);
-					const parentState = parentStatuses[0].state;
-					const stillFailed = parentState == 'failure' || parentState == 'error';
-					if (stillFailed) {
-						post(`**⚠️BUILD STILL FAILED⚠️**: ?[${commit.commit.message}](${commit.html_url})`);
-					} else {
-						post(`**🚨BUILD FAILED🚨**: →→→?[${commit.commit.message}](${commit.html_url})←←←`);
-					}
-				});
-				break;
-		}
-	});
-
-	handler.on('push', event => {
-		const ref = event.ref;
-		switch (ref) {
-			case 'refs/heads/master':
-				const pusher = event.pusher;
-				const compare = event.compare;
-				const commits = event.commits;
-				post([
-					`Pushed by **${pusher.name}** with ?[${commits.length} commit${commits.length > 1 ? 's' : ''}](${compare}):`,
-					commits.reverse().map(commit => `・[?[${commit.id.substr(0, 7)}](${commit.url})] ${commit.message.split('\n')[0]}`).join('\n'),
-				].join('\n'));
-				break;
-			case 'refs/heads/release':
-				const commit = event.commits[0];
-				post(`RELEASED: ${commit.message}`);
-				break;
-		}
-	});
-
-	handler.on('issues', event => {
-		const issue = event.issue;
-		const action = event.action;
-		let title: string;
-		switch (action) {
-			case 'opened': title = 'Issue opened'; break;
-			case 'closed': title = 'Issue closed'; break;
-			case 'reopened': title = 'Issue reopened'; break;
-			default: return;
-		}
-		post(`${title}: <${issue.number}>「${issue.title}」\n${issue.html_url}`);
-	});
-
-	handler.on('issue_comment', event => {
-		const issue = event.issue;
-		const comment = event.comment;
-		const action = event.action;
-		let text: string;
-		switch (action) {
-			case 'created': text = `Commented to「${issue.title}」:${comment.user.login}「${comment.body}」\n${comment.html_url}`; break;
-			default: return;
-		}
-		post(text);
-	});
-
-	handler.on('watch', event => {
-		const sender = event.sender;
-		post(`⭐️ Starred by **${sender.login}** ⭐️`);
-	});
-
-	handler.on('fork', event => {
-		const repo = event.forkee;
-		post(`🍴 Forked:\n${repo.html_url} 🍴`);
-	});
-
-	handler.on('pull_request', event => {
-		const pr = event.pull_request;
-		const action = event.action;
-		let text: string;
-		switch (action) {
-			case 'opened': text = `New Pull Request:「${pr.title}」\n${pr.html_url}`; break;
-			case 'reopened': text = `Pull Request Reopened:「${pr.title}」\n${pr.html_url}`; break;
-			case 'closed':
-				text = pr.merged
-					? `Pull Request Merged!:「${pr.title}」\n${pr.html_url}`
-					: `Pull Request Closed:「${pr.title}」\n${pr.html_url}`;
-				break;
-			default: return;
-		}
-		post(text);
-	});
+	createNote(bot, { text });
 };
+
+// Init router
+const router = new Router();
+
+if (config.github_bot != null) {
+	const secret = config.github_bot.hook_secret;
+
+	router.post('/hooks/github', ctx => {
+		const sig1 = new Buffer(ctx.headers['x-hub-signature']);
+		const sig2 = new Buffer(`sha1=${crypto.createHmac('sha1', secret).update(JSON.stringify(ctx.body)).digest('hex')}`);
+		if (sig1.equals(sig2)) {
+			handler.emit(ctx.headers['x-github-event'], ctx.body);
+			ctx.status = 204;
+		} else {
+			ctx.status = 400;
+		}
+	});
+}
+
+module.exports = router;
+
+handler.on('status', event => {
+	const state = event.state;
+	switch (state) {
+		case 'error':
+		case 'failure':
+			const commit = event.commit;
+			const parent = commit.parents[0];
+
+			// Fetch parent status
+			request({
+				url: `${parent.url}/statuses`,
+				headers: {
+					'User-Agent': 'misskey'
+				}
+			}, (err, res, body) => {
+				if (err) {
+					console.error(err);
+					return;
+				}
+				const parentStatuses = JSON.parse(body);
+				const parentState = parentStatuses[0].state;
+				const stillFailed = parentState == 'failure' || parentState == 'error';
+				if (stillFailed) {
+					post(`**⚠️BUILD STILL FAILED⚠️**: ?[${commit.commit.message}](${commit.html_url})`);
+				} else {
+					post(`**🚨BUILD FAILED🚨**: →→→?[${commit.commit.message}](${commit.html_url})←←←`);
+				}
+			});
+			break;
+	}
+});
+
+handler.on('push', event => {
+	const ref = event.ref;
+	switch (ref) {
+		case 'refs/heads/master':
+			const pusher = event.pusher;
+			const compare = event.compare;
+			const commits = event.commits;
+			post([
+				`Pushed by **${pusher.name}** with ?[${commits.length} commit${commits.length > 1 ? 's' : ''}](${compare}):`,
+				commits.reverse().map(commit => `・[?[${commit.id.substr(0, 7)}](${commit.url})] ${commit.message.split('\n')[0]}`).join('\n'),
+			].join('\n'));
+			break;
+		case 'refs/heads/release':
+			const commit = event.commits[0];
+			post(`RELEASED: ${commit.message}`);
+			break;
+	}
+});
+
+handler.on('issues', event => {
+	const issue = event.issue;
+	const action = event.action;
+	let title: string;
+	switch (action) {
+		case 'opened': title = 'Issue opened'; break;
+		case 'closed': title = 'Issue closed'; break;
+		case 'reopened': title = 'Issue reopened'; break;
+		default: return;
+	}
+	post(`${title}: <${issue.number}>「${issue.title}」\n${issue.html_url}`);
+});
+
+handler.on('issue_comment', event => {
+	const issue = event.issue;
+	const comment = event.comment;
+	const action = event.action;
+	let text: string;
+	switch (action) {
+		case 'created': text = `Commented to「${issue.title}」:${comment.user.login}「${comment.body}」\n${comment.html_url}`; break;
+		default: return;
+	}
+	post(text);
+});
+
+handler.on('watch', event => {
+	const sender = event.sender;
+	post(`⭐️ Starred by **${sender.login}** ⭐️`);
+});
+
+handler.on('fork', event => {
+	const repo = event.forkee;
+	post(`🍴 Forked:\n${repo.html_url} 🍴`);
+});
+
+handler.on('pull_request', event => {
+	const pr = event.pull_request;
+	const action = event.action;
+	let text: string;
+	switch (action) {
+		case 'opened': text = `New Pull Request:「${pr.title}」\n${pr.html_url}`; break;
+		case 'reopened': text = `Pull Request Reopened:「${pr.title}」\n${pr.html_url}`; break;
+		case 'closed':
+			text = pr.merged
+				? `Pull Request Merged!:「${pr.title}」\n${pr.html_url}`
+				: `Pull Request Closed:「${pr.title}」\n${pr.html_url}`;
+			break;
+		default: return;
+	}
+	post(text);
+});
diff --git a/src/server/api/service/twitter.ts b/src/server/api/service/twitter.ts
index e5239fa171..9fb01b44ef 100644
--- a/src/server/api/service/twitter.ts
+++ b/src/server/api/service/twitter.ts
@@ -1,160 +1,155 @@
-import * as express from 'express';
-import * as cookie from 'cookie';
+import * as Koa from 'koa';
+import * as Router from 'koa-router';
 import * as uuid from 'uuid';
-// import * as Twitter from 'twitter';
-// const Twitter = require('twitter');
 import autwh from 'autwh';
 import redis from '../../../db/redis';
-import User, { pack } from '../../../models/user';
+import User, { pack, ILocalUser } from '../../../models/user';
 import event from '../../../publishers/stream';
 import config from '../../../config';
 import signin from '../common/signin';
 
-module.exports = (app: express.Application) => {
-	function getUserToken(req: express.Request) {
-		// req.headers['cookie'] は常に string ですが、型定義の都合上
-		// string | string[] になっているので string を明示しています
-		return ((req.headers['cookie'] as string || '').match(/i=(!\w+)/) || [null, null])[1];
+function getUserToken(ctx: Koa.Context) {
+	return ((ctx.headers['cookie'] || '').match(/i=(!\w+)/) || [null, null])[1];
+}
+
+function compareOrigin(ctx: Koa.Context) {
+	function normalizeUrl(url: string) {
+		return url[url.length - 1] === '/' ? url.substr(0, url.length - 1) : url;
 	}
 
-	function compareOrigin(req: express.Request) {
-		function normalizeUrl(url: string) {
-			return url[url.length - 1] === '/' ? url.substr(0, url.length - 1) : url;
-		}
+	const referer = ctx.headers['referer'];
 
-		// req.headers['referer'] は常に string ですが、型定義の都合上
-		// string | string[] になっているので string を明示しています
-		const referer = req.headers['referer'] as string;
+	return (normalizeUrl(referer) == normalizeUrl(config.url));
+}
 
-		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({
-			host: null,
-			'token': userToken
-		}, {
-			$set: {
-				'twitter': null
-			}
-		});
-
-		res.send(`Twitterの連携を解除しました :v:`);
-
-		// Publish i updated event
-		event(user._id, 'i_updated', await pack(user, user, {
-			detail: true,
-			includeSecrets: true
-		}));
-	});
-
-	if (config.twitter == null) {
-		app.get('/connect/twitter', (req, res) => {
-			res.send('現在Twitterへ接続できません (このインスタンスではTwitterはサポートされていません)');
-		});
-
-		app.get('/signin/twitter', (req, res) => {
-			res.send('現在Twitterへ接続できません (このインスタンスではTwitterはサポートされていません)');
-		});
+// Init router
+const router = new Router();
 
+router.get('/disconnect/twitter', async ctx => {
+	if (!compareOrigin(ctx)) {
+		ctx.throw(400, 'invalid origin');
 		return;
 	}
 
+	const userToken = getUserToken(ctx);
+	if (userToken == null) {
+		ctx.throw(400, 'signin required');
+		return;
+	}
+
+	const user = await User.findOneAndUpdate({
+		host: null,
+		'token': userToken
+	}, {
+		$set: {
+			'twitter': null
+		}
+	});
+
+	ctx.body = `Twitterの連携を解除しました :v:`;
+
+	// Publish i updated event
+	event(user._id, 'i_updated', await pack(user, user, {
+		detail: true,
+		includeSecrets: true
+	}));
+});
+
+if (config.twitter == null) {
+	router.get('/connect/twitter', ctx => {
+		ctx.body = '現在Twitterへ接続できません (このインスタンスではTwitterはサポートされていません)';
+	});
+
+	router.get('/signin/twitter', ctx => {
+		ctx.body = '現在Twitterへ接続できません (このインスタンスではTwitterはサポートされていません)';
+	});
+} else {
 	const twAuth = autwh({
 		consumerKey: config.twitter.consumer_key,
 		consumerSecret: config.twitter.consumer_secret,
 		callbackUrl: `${config.url}/api/tw/cb`
 	});
 
-	app.get('/connect/twitter', async (req, res): Promise<any> => {
-		if (!compareOrigin(req)) {
-			res.status(400).send('invalid origin');
+	router.get('/connect/twitter', async ctx => {
+		if (!compareOrigin(ctx)) {
+			ctx.throw(400, 'invalid origin');
 			return;
 		}
 
-		const userToken = getUserToken(req);
-		if (userToken == null) return res.send('plz signin');
+		const userToken = getUserToken(ctx);
+		if (userToken == null) {
+			ctx.throw(400, 'signin required');
+			return;
+		}
 
-		const ctx = await twAuth.begin();
-		redis.set(userToken, JSON.stringify(ctx));
-		res.redirect(ctx.url);
+		const twCtx = await twAuth.begin();
+		redis.set(userToken, JSON.stringify(twCtx));
+		ctx.redirect(twCtx.url);
 	});
 
-	app.get('/signin/twitter', async (req, res): Promise<any> => {
-		const ctx = await twAuth.begin();
+	router.get('/signin/twitter', async ctx => {
+		const twCtx = await twAuth.begin();
 
 		const sessid = uuid();
 
-		redis.set(sessid, JSON.stringify(ctx));
+		redis.set(sessid, JSON.stringify(twCtx));
 
 		const expires = 1000 * 60 * 60; // 1h
-		res.cookie('signin_with_twitter_session_id', sessid, {
+		ctx.cookies.set('signin_with_twitter_session_id', sessid, {
 			path: '/',
-			domain: `.${config.host}`,
-			secure: config.url.substr(0, 5) === 'https',
+			domain: config.host,
+			secure: config.url.startsWith('https'),
 			httpOnly: true,
 			expires: new Date(Date.now() + expires),
 			maxAge: expires
 		});
 
-		res.redirect(ctx.url);
+		ctx.redirect(twCtx.url);
 	});
 
-	app.get('/tw/cb', (req, res): any => {
-		const userToken = getUserToken(req);
+	router.get('/tw/cb', ctx => {
+		const userToken = getUserToken(ctx);
 
 		if (userToken == null) {
-			// req.headers['cookie'] は常に string ですが、型定義の都合上
-			// string | string[] になっているので string を明示しています
-			const cookies = cookie.parse((req.headers['cookie'] as string || ''));
+			const sessid = ctx.cookies.get('signin_with_twitter_session_id');
 
-			const sessid = cookies['signin_with_twitter_session_id'];
-
-			if (sessid == undefined) {
-				res.status(400).send('invalid session');
+			if (sessid == null) {
+				ctx.throw(400, 'invalid session');
 				return;
 			}
 
-			redis.get(sessid, async (_, ctx) => {
-				const result = await twAuth.done(JSON.parse(ctx), req.query.oauth_verifier);
+			redis.get(sessid, async (_, twCtx) => {
+				const result = await twAuth.done(JSON.parse(twCtx), ctx.query.oauth_verifier);
 
 				const user = await User.findOne({
 					host: null,
 					'twitter.userId': result.userId
-				});
+				}) as ILocalUser;
 
 				if (user == null) {
-					res.status(404).send(`@${result.screenName}と連携しているMisskeyアカウントはありませんでした...`);
+					ctx.throw(404, `@${result.screenName}と連携しているMisskeyアカウントはありませんでした...`);
 					return;
 				}
 
-				signin(res, user, true);
+				signin(ctx, user, true);
 			});
 		} else {
-			const verifier = req.query.oauth_verifier;
+			const verifier = ctx.query.oauth_verifier;
 
 			if (verifier == null) {
-				res.status(400).send('invalid session');
+				ctx.throw(400, 'invalid session');
 				return;
 			}
 
-			redis.get(userToken, async (_, ctx) => {
-				const result = await twAuth.done(JSON.parse(ctx), verifier);
+			redis.get(userToken, async (_, twCtx) => {
+				const result = await twAuth.done(JSON.parse(twCtx), verifier);
 
 				const user = await User.findOneAndUpdate({
 					host: null,
-					'token': userToken
+					token: userToken
 				}, {
 					$set: {
-						'twitter': {
+						twitter: {
 							accessToken: result.accessToken,
 							accessTokenSecret: result.accessTokenSecret,
 							userId: result.userId,
@@ -163,7 +158,7 @@ module.exports = (app: express.Application) => {
 					}
 				});
 
-				res.send(`Twitter: @${result.screenName} を、Misskey: @${user.username} に接続しました!`);
+				ctx.body = `Twitter: @${result.screenName} を、Misskey: @${user.username} に接続しました!`;
 
 				// Publish i updated event
 				event(user._id, 'i_updated', await pack(user, user, {
@@ -173,4 +168,6 @@ module.exports = (app: express.Application) => {
 			});
 		}
 	});
-};
+}
+
+module.exports = router;
diff --git a/src/server/file/index.ts b/src/server/file/index.ts
index 95e7867f01..d58939f1be 100644
--- a/src/server/file/index.ts
+++ b/src/server/file/index.ts
@@ -3,171 +3,33 @@
  */
 
 import * as fs from 'fs';
-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 stream from 'stream';
+import * as Koa from 'koa';
+import * as cors from '@koa/cors';
+import * as Router from 'koa-router';
+import pour from './pour';
+import sendDriveFile from './send-drive-file';
 
-import DriveFile, { getGridFSBucket } from '../../models/drive-file';
-
-const gm = _gm.subClass({
-	imageMagick: true
-});
-
-/**
- * Init app
- */
-const app = express();
-
-app.disable('x-powered-by');
-app.locals.cache = true;
-app.use(bodyParser.urlencoded({ extended: true }));
+// Init app
+const app = new Koa();
 app.use(cors());
 
-/**
- * Statics
- */
-app.use('/assets', express.static(`${__dirname}/assets`, {
-	maxAge: 1000 * 60 * 60 * 24 * 365 // 一年
-}));
+// Init router
+const router = new Router();
 
-app.get('/', (req, res) => {
-	res.send('yee haw');
-});
-
-app.get('/default-avatar.jpg', (req, res) => {
+router.get('/default-avatar.jpg', ctx => {
 	const file = fs.createReadStream(`${__dirname}/assets/avatar.jpg`);
-	send(file, 'image/jpeg', req, res);
+	pour(file, 'image/jpeg', ctx);
 });
 
-app.get('/app-default.jpg', (req, res) => {
+router.get('/app-default.jpg', ctx => {
 	const file = fs.createReadStream(`${__dirname}/assets/dummy.png`);
-	send(file, 'image/png', req, res);
+	pour(file, 'image/png', ctx);
 });
 
-interface ISend {
-	contentType: string;
-	stream: stream.Readable;
-}
+router.get('/:id', sendDriveFile);
+router.get('/:id/:name', sendDriveFile);
 
-function thumbnail(data: stream.Readable, type: string, resize: number): ISend {
-	const readable: stream.Readable = (() => {
-		// 動画であれば
-		if (/^video\/.*$/.test(type)) {
-			// TODO
-			// 使わないことになったストリームはしっかり取り壊す
-			data.destroy();
-			return fs.createReadStream(`${__dirname}/assets/thumbnail-not-available.png`);
-		// 画像であれば
-		// Note: SVGはapplication/xml
-		} else if (/^image\/.*$/.test(type) || type == 'application/xml') {
-			// 0フレーム目を送る
-			try {
-				return gm(data).selectFrame(0).stream();
-			// だめだったら
-			} catch (e) {
-				// 使わないことになったストリームはしっかり取り壊す
-				data.destroy();
-				return fs.createReadStream(`${__dirname}/assets/thumbnail-not-available.png`);
-			}
-		// 動画か画像以外
-		} else {
-			data.destroy();
-			return fs.createReadStream(`${__dirname}/assets/not-an-image.png`);
-		}
-	})();
-
-	let g = gm(readable);
-
-	if (resize) {
-		g = g.resize(resize, resize);
-	}
-
-	const stream = g
-		.compress('jpeg')
-		.quality(80)
-		.interlace('line')
-		.stream();
-
-	return {
-		contentType: 'image/jpeg',
-		stream
-	};
-}
-
-const commonReadableHandlerGenerator = (req: express.Request, res: express.Response) => (e: Error): void => {
-	console.dir(e);
-	req.destroy();
-	res.destroy(e);
-};
-
-function send(readable: stream.Readable, type: string, req: express.Request, res: express.Response): void {
-	readable.on('error', commonReadableHandlerGenerator(req, res));
-
-	const data = ((): ISend => {
-		if (req.query.thumbnail !== undefined) {
-			return thumbnail(readable, type, req.query.size);
-		}
-		return {
-			contentType: type,
-			stream: readable
-		};
-	})();
-
-	if (readable !== data.stream) {
-		data.stream.on('error', commonReadableHandlerGenerator(req, res));
-	}
-
-	if (req.query.download !== undefined) {
-		res.header('Content-Disposition', 'attachment');
-	}
-
-	res.header('Content-Type', data.contentType);
-
-	data.stream.pipe(res);
-
-	data.stream.on('end', () => {
-		res.end();
-	});
-}
-
-async function sendFileById(req: express.Request, res: express.Response): Promise<void> {
-	// Validate id
-	if (!mongodb.ObjectID.isValid(req.params.id)) {
-		res.status(400).send('incorrect id');
-		return;
-	}
-
-	const fileId = new mongodb.ObjectID(req.params.id);
-
-	// Fetch (drive) file
-	const file = await DriveFile.findOne({ _id: fileId });
-
-	// validate name
-	if (req.params.name !== undefined && req.params.name !== file.filename) {
-		res.status(404).send('there is no file has given name');
-		return;
-	}
-
-	if (file == null) {
-		res.status(404).sendFile(`${__dirname}/assets/dummy.png`);
-		return;
-	}
-
-	const bucket = await getGridFSBucket();
-
-	const readable = bucket.openDownloadStream(fileId);
-
-	send(readable, file.contentType, req, res);
-}
-
-/**
- * Routing
- */
-
-app.get('/:id', sendFileById);
-app.get('/:id/:name', sendFileById);
+// Register router
+app.use(router.routes());
 
 module.exports = app;
diff --git a/src/server/file/pour.ts b/src/server/file/pour.ts
new file mode 100644
index 0000000000..2a31cb5898
--- /dev/null
+++ b/src/server/file/pour.ts
@@ -0,0 +1,93 @@
+import * as fs from 'fs';
+import * as stream from 'stream';
+import * as Koa from 'koa';
+import * as Gm from 'gm';
+
+const gm = Gm.subClass({
+	imageMagick: true
+});
+
+interface ISend {
+	contentType: string;
+	stream: stream.Readable;
+}
+
+function thumbnail(data: stream.Readable, type: string, resize: number): ISend {
+	const readable: stream.Readable = (() => {
+		// 動画であれば
+		if (/^video\/.*$/.test(type)) {
+			// TODO
+			// 使わないことになったストリームはしっかり取り壊す
+			data.destroy();
+			return fs.createReadStream(`${__dirname}/assets/thumbnail-not-available.png`);
+		// 画像であれば
+		// Note: SVGはapplication/xml
+		} else if (/^image\/.*$/.test(type) || type == 'application/xml') {
+			// 0フレーム目を送る
+			try {
+				return gm(data).selectFrame(0).stream();
+			// だめだったら
+			} catch (e) {
+				// 使わないことになったストリームはしっかり取り壊す
+				data.destroy();
+				return fs.createReadStream(`${__dirname}/assets/thumbnail-not-available.png`);
+			}
+		// 動画か画像以外
+		} else {
+			data.destroy();
+			return fs.createReadStream(`${__dirname}/assets/not-an-image.png`);
+		}
+	})();
+
+	let g = gm(readable);
+
+	if (resize) {
+		g = g.resize(resize, resize);
+	}
+
+	const stream = g
+		.compress('jpeg')
+		.quality(80)
+		.interlace('line')
+		.stream();
+
+	return {
+		contentType: 'image/jpeg',
+		stream
+	};
+}
+
+const commonReadableHandlerGenerator = (ctx: Koa.Context) => (e: Error): void => {
+	console.error(e);
+	ctx.status = 500;
+};
+
+export default function(readable: stream.Readable, type: string, ctx: Koa.Context): void {
+	readable.on('error', commonReadableHandlerGenerator(ctx));
+
+	const data = ((): ISend => {
+		if (ctx.query.thumbnail !== undefined) {
+			return thumbnail(readable, type, ctx.query.size);
+		}
+		return {
+			contentType: type,
+			stream: readable
+		};
+	})();
+
+	if (readable !== data.stream) {
+		data.stream.on('error', commonReadableHandlerGenerator(ctx));
+	}
+
+	if (ctx.query.download !== undefined) {
+		ctx.header('Content-Disposition', 'attachment');
+	}
+
+	ctx.header('Content-Type', data.contentType);
+
+	data.stream.pipe(ctx.res);
+
+	data.stream.on('end', () => {
+		ctx.res.end();
+	});
+}
diff --git a/src/server/file/send-drive-file.ts b/src/server/file/send-drive-file.ts
new file mode 100644
index 0000000000..e6ee19ff1d
--- /dev/null
+++ b/src/server/file/send-drive-file.ts
@@ -0,0 +1,30 @@
+import * as Koa from 'koa';
+import * as send from 'koa-send';
+import * as mongodb from 'mongodb';
+import DriveFile, { getGridFSBucket } from '../../models/drive-file';
+import pour from './pour';
+
+export default async function(ctx: Koa.Context) {
+	// Validate id
+	if (!mongodb.ObjectID.isValid(ctx.params.id)) {
+		ctx.throw(400, 'incorrect id');
+		return;
+	}
+
+	const fileId = new mongodb.ObjectID(ctx.params.id);
+
+	// Fetch drive file
+	const file = await DriveFile.findOne({ _id: fileId });
+
+	if (file == null) {
+		ctx.status = 404;
+		await send(ctx, `${__dirname}/assets/dummy.png`);
+		return;
+	}
+
+	const bucket = await getGridFSBucket();
+
+	const readable = bucket.openDownloadStream(fileId);
+
+	pour(readable, file.contentType, ctx);
+}
diff --git a/src/server/index.ts b/src/server/index.ts
index e9bfa9e10b..f0d60fa651 100644
--- a/src/server/index.ts
+++ b/src/server/index.ts
@@ -13,7 +13,7 @@ import activityPub from './activitypub';
 import webFinger from './webfinger';
 import config from '../config';
 
-// Init server
+// Init app
 const app = new Koa();
 app.proxy = true;
 app.use(bodyParser);
@@ -46,9 +46,9 @@ function createServer() {
 		Object.keys(config.https).forEach(k => {
 			certs[k] = fs.readFileSync(config.https[k]);
 		});
-		return https.createServer(certs, app);
+		return https.createServer(certs, app.callback);
 	} else {
-		return http.createServer(app);
+		return http.createServer(app.callback);
 	}
 }
 
diff --git a/src/server/web/docs.ts b/src/server/web/docs.ts
index 889532e17e..a546d1e88c 100644
--- a/src/server/web/docs.ts
+++ b/src/server/web/docs.ts
@@ -1,24 +1,21 @@
 /**
- * Docs Server
+ * Docs
  */
 
 import * as path from 'path';
-import * as express from 'express';
+import * as Router from 'koa-router';
+import * as send from 'koa-send';
 
 const docs = path.resolve(`${__dirname}/../../client/docs/`);
 
-/**
- * Init app
- */
-const app = express();
-app.disable('x-powered-by');
+const router = new Router();
 
-app.use('/assets', express.static(`${docs}/assets`));
+router.get('/assets', async ctx => {
+	await send(ctx, `${docs}/assets`);
+});
 
-/**
- * Routing
- */
-app.get(/^\/([a-z_\-\/]+?)$/, (req, res) =>
-	res.sendFile(`${docs}/${req.params[0]}.html`));
+router.get(/^\/([a-z_\-\/]+?)$/, async ctx => {
+	await send(ctx, `${docs}/${ctx.params[0]}.html`);
+});
 
-module.exports = app;
+module.exports = router;
diff --git a/src/server/web/index.ts b/src/server/web/index.ts
index 5b1b6409b9..b28ad5592c 100644
--- a/src/server/web/index.ts
+++ b/src/server/web/index.ts
@@ -5,60 +5,71 @@
 import * as path from 'path';
 import ms = require('ms');
 
-// express modules
-import * as express from 'express';
-import * as bodyParser from 'body-parser';
-import * as favicon from 'serve-favicon';
-import * as compression from 'compression';
+import * as Koa from 'koa';
+import * as Router from 'koa-router';
+import * as send from 'koa-send';
+import * as favicon from 'koa-favicon';
 
 const client = path.resolve(`${__dirname}/../../client/`);
 
-// Create server
-const app = express();
-app.disable('x-powered-by');
+// Init app
+const app = new Koa();
 
-app.use('/docs', require('./docs'));
+// Serve favicon
+app.use(favicon(`${client}/assets/favicon.ico`));
 
-app.use(bodyParser.urlencoded({ extended: true }));
-app.use(bodyParser.json({
-	type: ['application/json', 'text/plain']
-}));
-app.use(compression());
-
-app.use((req, res, next) => {
-	res.header('X-Frame-Options', 'DENY');
+// Common request handler
+app.use((ctx, next) => {
+	// IFrameの中に入れられないようにする
+	ctx.set('X-Frame-Options', 'DENY');
 	next();
 });
 
+// Init router
+const router = new Router();
+
 //#region static assets
 
-app.use(favicon(`${client}/assets/favicon.ico`));
-app.get('/apple-touch-icon.png', (req, res) => res.sendFile(`${client}/assets/apple-touch-icon.png`));
-app.use('/assets', express.static(`${client}/assets`, {
-	maxAge: ms('7 days')
-}));
-app.use('/assets/*.js', (req, res) => res.sendFile(`${client}/assets/404.js`));
-app.use('/assets', (req, res) => {
-	res.sendStatus(404);
-});
-
-// ServiceWroker
-app.get(/^\/sw\.(.+?)\.js$/, (req, res) =>
-	res.sendFile(`${client}/assets/sw.${req.params[0]}.js`));
-
-// Manifest
-app.get('/manifest.json', (req, res) =>
-	res.sendFile(`${client}/assets/manifest.json`));
-
-//#endregion
-
-app.get(/\/api:url/, require('./url-preview'));
-
-// Render base html for all requests
-app.get('*', (req, res) => {
-	res.sendFile(path.resolve(`${client}/app/base.html`), {
-		maxAge: ms('7 days')
+router.get('/assets', async ctx => {
+	await send(ctx, ctx.path, {
+		root: `${client}/assets`,
+		maxage: ms('7 days'),
+		immutable: true
 	});
 });
 
+// Apple touch icon
+router.get('/apple-touch-icon.png', async ctx => {
+	await send(ctx, `${client}/assets/apple-touch-icon.png`);
+});
+
+// ServiceWroker
+router.get(/^\/sw\.(.+?)\.js$/, async ctx => {
+	await send(ctx, `${client}/assets/sw.${ctx.params[0]}.js`);
+});
+
+// Manifest
+router.get('/manifest.json', async ctx => {
+	await send(ctx, `${client}/assets/manifest.json`);
+});
+
+//#endregion
+
+// Docs
+router.use('/docs', require('./docs').routes());
+
+// URL preview endpoint
+router.get('url', require('./url-preview'));
+
+// Render base html for all requests
+router.get('*', async ctx => {
+	await send(ctx, `${client}/app/base.html`, {
+		maxage: ms('7 days'),
+		immutable: true
+	});
+});
+
+// Register router
+app.use(router.routes());
+
 module.exports = app;
diff --git a/src/server/web/url-preview.ts b/src/server/web/url-preview.ts
index 0c5fd8a78e..4b3f44a5da 100644
--- a/src/server/web/url-preview.ts
+++ b/src/server/web/url-preview.ts
@@ -1,11 +1,11 @@
-import * as express from 'express';
+import * as Koa from 'koa';
 import summaly from 'summaly';
 
-module.exports = async (req: express.Request, res: express.Response) => {
-	const summary = await summaly(req.query.url);
+module.exports = async (ctx: Koa.Context) => {
+	const summary = await summaly(ctx.query.url);
 	summary.icon = wrap(summary.icon);
 	summary.thumbnail = wrap(summary.thumbnail);
-	res.send(summary);
+	ctx.body = summary;
 };
 
 function wrap(url: string): string {

From c54ff5b2bced1dd3d15f98e4a8a40523a0fcea78 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 13 Apr 2018 06:17:14 +0900
Subject: [PATCH 22/34] wip

---
 package.json        | 1 +
 src/server/index.ts | 9 ++++++---
 2 files changed, 7 insertions(+), 3 deletions(-)

diff --git a/package.json b/package.json
index 3e349203fc..b611f603c9 100644
--- a/package.json
+++ b/package.json
@@ -150,6 +150,7 @@
 		"js-yaml": "3.11.0",
 		"jsdom": "11.7.0",
 		"koa": "^2.5.0",
+		"koa-bodyparser": "^4.2.0",
 		"koa-favicon": "^2.0.1",
 		"koa-mount": "^3.0.0",
 		"koa-multer": "^1.0.2",
diff --git a/src/server/index.ts b/src/server/index.ts
index f0d60fa651..02362037ee 100644
--- a/src/server/index.ts
+++ b/src/server/index.ts
@@ -8,6 +8,7 @@ import * as https from 'https';
 import * as Koa from 'koa';
 import * as Router from 'koa-router';
 import * as bodyParser from 'koa-bodyparser';
+import * as mount from 'koa-mount';
 
 import activityPub from './activitypub';
 import webFinger from './webfinger';
@@ -27,15 +28,17 @@ if (config.url.startsWith('https')) {
 	});
 }
 
+app.use(mount('/api', require('./api')));
+app.use(mount('/files', require('./file')));
+
 // Init router
 const router = new Router();
 
 // Routing
-router.use('/api', require('./api'));
-router.use('/files', require('./file'));
 router.use(activityPub.routes());
 router.use(webFinger.routes());
-router.use(require('./web'));
+
+app.use(mount(require('./web')));
 
 // Register router
 app.use(router.routes());

From 61f21594a9edbb3102d6a0f45c1e95ed82c3f606 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 13 Apr 2018 07:34:27 +0900
Subject: [PATCH 23/34] wip

---
 src/server/api/index.ts |  2 ++
 src/server/file/pour.ts |  5 +++--
 src/server/index.ts     | 16 +++++++---------
 src/server/web/index.ts | 17 ++++++++---------
 4 files changed, 20 insertions(+), 20 deletions(-)

diff --git a/src/server/api/index.ts b/src/server/api/index.ts
index d2427d30ae..c383e1cf8d 100644
--- a/src/server/api/index.ts
+++ b/src/server/api/index.ts
@@ -5,6 +5,7 @@
 import * as Koa from 'koa';
 import * as Router from 'koa-router';
 import * as multer from 'koa-multer';
+import * as bodyParser from 'koa-bodyparser';
 
 import endpoints from './endpoints';
 
@@ -12,6 +13,7 @@ const handler = require('./api-handler').default;
 
 // Init app
 const app = new Koa();
+app.use(bodyParser);
 
 // Init multer instance
 const upload = multer({
diff --git a/src/server/file/pour.ts b/src/server/file/pour.ts
index 2a31cb5898..b38b969c2d 100644
--- a/src/server/file/pour.ts
+++ b/src/server/file/pour.ts
@@ -80,10 +80,11 @@ export default function(readable: stream.Readable, type: string, ctx: Koa.Contex
 	}
 
 	if (ctx.query.download !== undefined) {
-		ctx.header('Content-Disposition', 'attachment');
+		ctx.set('Content-Disposition', 'attachment');
 	}
 
-	ctx.header('Content-Type', data.contentType);
+	ctx.set('Cache-Control', 'max-age=31536000, immutable');
+	ctx.set('Content-Type', data.contentType);
 
 	data.stream.pipe(ctx.res);
 
diff --git a/src/server/index.ts b/src/server/index.ts
index 02362037ee..a637e8598b 100644
--- a/src/server/index.ts
+++ b/src/server/index.ts
@@ -4,10 +4,9 @@
 
 import * as fs from 'fs';
 import * as http from 'http';
-import * as https from 'https';
+import * as http2 from 'http2';
 import * as Koa from 'koa';
 import * as Router from 'koa-router';
-import * as bodyParser from 'koa-bodyparser';
 import * as mount from 'koa-mount';
 
 import activityPub from './activitypub';
@@ -17,14 +16,13 @@ import config from '../config';
 // Init app
 const app = new Koa();
 app.proxy = true;
-app.use(bodyParser);
 
 // HSTS
 // 6months (15552000sec)
 if (config.url.startsWith('https')) {
-	app.use((ctx, next) => {
+	app.use(async (ctx, next) => {
 		ctx.set('strict-transport-security', 'max-age=15552000; preload');
-		next();
+		await next();
 	});
 }
 
@@ -38,20 +36,20 @@ const router = new Router();
 router.use(activityPub.routes());
 router.use(webFinger.routes());
 
-app.use(mount(require('./web')));
-
 // Register router
 app.use(router.routes());
 
+app.use(mount(require('./web')));
+
 function createServer() {
 	if (config.https) {
 		const certs = {};
 		Object.keys(config.https).forEach(k => {
 			certs[k] = fs.readFileSync(config.https[k]);
 		});
-		return https.createServer(certs, app.callback);
+		return http2.createSecureServer(certs, app.callback());
 	} else {
-		return http.createServer(app.callback);
+		return http.createServer(app.callback());
 	}
 }
 
diff --git a/src/server/web/index.ts b/src/server/web/index.ts
index b28ad5592c..dd296f875d 100644
--- a/src/server/web/index.ts
+++ b/src/server/web/index.ts
@@ -2,15 +2,13 @@
  * Web Client Server
  */
 
-import * as path from 'path';
 import ms = require('ms');
-
 import * as Koa from 'koa';
 import * as Router from 'koa-router';
 import * as send from 'koa-send';
 import * as favicon from 'koa-favicon';
 
-const client = path.resolve(`${__dirname}/../../client/`);
+const client = `${__dirname}/../../client/`;
 
 // Init app
 const app = new Koa();
@@ -19,10 +17,10 @@ const app = new Koa();
 app.use(favicon(`${client}/assets/favicon.ico`));
 
 // Common request handler
-app.use((ctx, next) => {
+app.use(async (ctx, next) => {
 	// IFrameの中に入れられないようにする
 	ctx.set('X-Frame-Options', 'DENY');
-	next();
+	await next();
 });
 
 // Init router
@@ -30,9 +28,9 @@ const router = new Router();
 
 //#region static assets
 
-router.get('/assets', async ctx => {
+router.get('/assets/*', async ctx => {
 	await send(ctx, ctx.path, {
-		root: `${client}/assets`,
+		root: client,
 		maxage: ms('7 days'),
 		immutable: true
 	});
@@ -63,8 +61,9 @@ router.get('url', require('./url-preview'));
 
 // Render base html for all requests
 router.get('*', async ctx => {
-	await send(ctx, `${client}/app/base.html`, {
-		maxage: ms('7 days'),
+	await send(ctx, `app/base.html`, {
+		root: client,
+		maxage: ms('3 days'),
 		immutable: true
 	});
 });

From 22d2f2051c4cbe3da5b9ece674f36a6555f8c953 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 13 Apr 2018 09:44:00 +0900
Subject: [PATCH 24/34] wip

---
 src/client/app/common/mios.ts         |  5 +++--
 src/server/api/api-handler.ts         | 16 +++++++++++++---
 src/server/api/bot/interfaces/line.ts |  2 +-
 src/server/api/call.ts                |  4 +---
 src/server/api/index.ts               |  4 +++-
 src/server/api/private/signin.ts      |  6 +++---
 src/server/api/private/signup.ts      |  6 +++---
 src/server/api/service/github.ts      |  8 ++++++--
 src/server/file/index.ts              |  7 ++++++-
 src/server/file/pour.ts               |  8 +-------
 10 files changed, 40 insertions(+), 26 deletions(-)

diff --git a/src/client/app/common/mios.ts b/src/client/app/common/mios.ts
index a09af799be..ccc73eebc3 100644
--- a/src/client/app/common/mios.ts
+++ b/src/client/app/common/mios.ts
@@ -444,9 +444,10 @@ export default class MiOS extends EventEmitter {
 		// Append a credential
 		if (this.isSignedIn) (data as any).i = this.i.token;
 
-		const viaStream = localStorage.getItem('apiViaStream') ? localStorage.getItem('apiViaStream') == 'true' : true;
-
 		return new Promise((resolve, reject) => {
+			const viaStream = this.stream.hasConnection &&
+				(localStorage.getItem('apiViaStream') ? localStorage.getItem('apiViaStream') == 'true' : true);
+
 			if (viaStream) {
 				const stream = this.stream.borrow();
 				const id = Math.random().toString();
diff --git a/src/server/api/api-handler.ts b/src/server/api/api-handler.ts
index 2c50234317..947794a20e 100644
--- a/src/server/api/api-handler.ts
+++ b/src/server/api/api-handler.ts
@@ -25,11 +25,21 @@ export default async (endpoint: Endpoint, ctx: Koa.Context) => {
 
 	// Authentication
 	try {
-		[user, app] = await authenticate(ctx.body['i']);
+		[user, app] = await authenticate(ctx.request.body['i']);
 	} catch (e) {
-		return reply(403, 'AUTHENTICATION_FAILED');
+		reply(403, 'AUTHENTICATION_FAILED');
+		return;
 	}
 
+	let res;
+
 	// API invoking
-	call(endpoint, user, app, ctx.body, ctx.req).then(reply).catch(e => reply(400, e));
+	try {
+		res = await call(endpoint, user, app, ctx.request.body, ctx.req);
+	} catch (e) {
+		reply(400, e);
+		return;
+	}
+
+	reply(res);
 };
diff --git a/src/server/api/bot/interfaces/line.ts b/src/server/api/bot/interfaces/line.ts
index 454630161a..733315391d 100644
--- a/src/server/api/bot/interfaces/line.ts
+++ b/src/server/api/bot/interfaces/line.ts
@@ -226,7 +226,7 @@ if (config.line_bot) {
 
 		// シグネチャ比較
 		if (sig1 === sig2) {
-			ctx.body.events.forEach(ev => {
+			ctx.request.body.events.forEach(ev => {
 				handler.emit('event', ev);
 			});
 		} else {
diff --git a/src/server/api/call.ts b/src/server/api/call.ts
index c25f55ed3f..cc40294657 100644
--- a/src/server/api/call.ts
+++ b/src/server/api/call.ts
@@ -6,11 +6,9 @@ import limitter from './limitter';
 import { IUser } from '../../models/user';
 import { IApp } from '../../models/app';
 
-export default (endpoint: string | Endpoint, user: IUser, app: IApp, data: any, req?: http.IncomingMessage) => new Promise(async (ok, rej) => {
+export default (endpoint: string | Endpoint, user: IUser, app: IApp, data: any, req?: http.IncomingMessage) => new Promise<any>(async (ok, rej) => {
 	const isSecure = user != null && app == null;
 
-	//console.log(endpoint, user, app, data);
-
 	const ep = typeof endpoint == 'string' ? endpoints.find(e => e.name == endpoint) : endpoint;
 
 	if (ep.secure && !isSecure) {
diff --git a/src/server/api/index.ts b/src/server/api/index.ts
index c383e1cf8d..2ea5fccb5b 100644
--- a/src/server/api/index.ts
+++ b/src/server/api/index.ts
@@ -13,7 +13,9 @@ const handler = require('./api-handler').default;
 
 // Init app
 const app = new Koa();
-app.use(bodyParser);
+app.use(bodyParser({
+	detectJSON: () => true
+}));
 
 // Init multer instance
 const upload = multer({
diff --git a/src/server/api/private/signin.ts b/src/server/api/private/signin.ts
index 55326deeaf..1737007206 100644
--- a/src/server/api/private/signin.ts
+++ b/src/server/api/private/signin.ts
@@ -11,9 +11,9 @@ export default async (ctx: Koa.Context) => {
 	ctx.set('Access-Control-Allow-Origin', config.url);
 	ctx.set('Access-Control-Allow-Credentials', 'true');
 
-	const username = ctx.body['username'];
-	const password = ctx.body['password'];
-	const token = ctx.body['token'];
+	const username = ctx.request.body['username'];
+	const password = ctx.request.body['password'];
+	const token = ctx.request.body['token'];
 
 	if (typeof username != 'string') {
 		ctx.status = 400;
diff --git a/src/server/api/private/signup.ts b/src/server/api/private/signup.ts
index a4554be4ae..15257b869f 100644
--- a/src/server/api/private/signup.ts
+++ b/src/server/api/private/signup.ts
@@ -37,7 +37,7 @@ export default async (ctx: Koa.Context) => {
 	// Verify recaptcha
 	// ただしテスト時はこの機構は障害となるため無効にする
 	if (process.env.NODE_ENV !== 'test') {
-		const success = await recaptcha(ctx.body['g-recaptcha-response']);
+		const success = await recaptcha(ctx.request.body['g-recaptcha-response']);
 
 		if (!success) {
 			ctx.throw(400, 'recaptcha-failed');
@@ -45,8 +45,8 @@ export default async (ctx: Koa.Context) => {
 		}
 	}
 
-	const username = ctx.body['username'];
-	const password = ctx.body['password'];
+	const username = ctx.request.body['username'];
+	const password = ctx.request.body['password'];
 
 	// Validate username
 	if (!validateUsername(username)) {
diff --git a/src/server/api/service/github.ts b/src/server/api/service/github.ts
index ee226cc5cc..cd9760a36d 100644
--- a/src/server/api/service/github.ts
+++ b/src/server/api/service/github.ts
@@ -35,10 +35,14 @@ if (config.github_bot != null) {
 	const secret = config.github_bot.hook_secret;
 
 	router.post('/hooks/github', ctx => {
+		const body = JSON.stringify(ctx.request.body);
+		const hash = crypto.createHmac('sha1', secret).update(body).digest('hex');
 		const sig1 = new Buffer(ctx.headers['x-hub-signature']);
-		const sig2 = new Buffer(`sha1=${crypto.createHmac('sha1', secret).update(JSON.stringify(ctx.body)).digest('hex')}`);
+		const sig2 = new Buffer(`sha1=${hash}`);
+
+		// シグネチャ比較
 		if (sig1.equals(sig2)) {
-			handler.emit(ctx.headers['x-github-event'], ctx.body);
+			handler.emit(ctx.headers['x-github-event'], ctx.request.body);
 			ctx.status = 204;
 		} else {
 			ctx.status = 400;
diff --git a/src/server/file/index.ts b/src/server/file/index.ts
index d58939f1be..d305286d12 100644
--- a/src/server/file/index.ts
+++ b/src/server/file/index.ts
@@ -13,6 +13,11 @@ import sendDriveFile from './send-drive-file';
 const app = new Koa();
 app.use(cors());
 
+app.use(async (ctx, next) => {
+	ctx.set('Cache-Control', 'max-age=31536000, immutable');
+	await next();
+});
+
 // Init router
 const router = new Router();
 
@@ -27,7 +32,7 @@ router.get('/app-default.jpg', ctx => {
 });
 
 router.get('/:id', sendDriveFile);
-router.get('/:id/:name', sendDriveFile);
+router.get('/:id/*', sendDriveFile);
 
 // Register router
 app.use(router.routes());
diff --git a/src/server/file/pour.ts b/src/server/file/pour.ts
index b38b969c2d..0fd0ad0e60 100644
--- a/src/server/file/pour.ts
+++ b/src/server/file/pour.ts
@@ -83,12 +83,6 @@ export default function(readable: stream.Readable, type: string, ctx: Koa.Contex
 		ctx.set('Content-Disposition', 'attachment');
 	}
 
-	ctx.set('Cache-Control', 'max-age=31536000, immutable');
 	ctx.set('Content-Type', data.contentType);
-
-	data.stream.pipe(ctx.res);
-
-	data.stream.on('end', () => {
-		ctx.res.end();
-	});
+	ctx.body = data.stream;
 }

From e2e7babee0de35385eb74830c82eaccdb28f013a Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 13 Apr 2018 11:44:39 +0900
Subject: [PATCH 25/34] wip

---
 src/server/api/api-handler.ts    | 6 ++++--
 src/server/api/call.ts           | 7 +++----
 src/server/api/common/signin.ts  | 4 +++-
 src/server/api/index.ts          | 3 ++-
 src/server/api/private/signin.ts | 4 ++--
 5 files changed, 14 insertions(+), 10 deletions(-)

diff --git a/src/server/api/api-handler.ts b/src/server/api/api-handler.ts
index 947794a20e..e716dcdc01 100644
--- a/src/server/api/api-handler.ts
+++ b/src/server/api/api-handler.ts
@@ -7,6 +7,8 @@ import { IUser } from '../../models/user';
 import { IApp } from '../../models/app';
 
 export default async (endpoint: Endpoint, ctx: Koa.Context) => {
+	const body = ctx.is('multipart/form-data') ? (ctx.req as any).body : ctx.request.body;
+
 	const reply = (x?: any, y?: any) => {
 		if (x === undefined) {
 			ctx.status = 204;
@@ -25,7 +27,7 @@ export default async (endpoint: Endpoint, ctx: Koa.Context) => {
 
 	// Authentication
 	try {
-		[user, app] = await authenticate(ctx.request.body['i']);
+		[user, app] = await authenticate(body['i']);
 	} catch (e) {
 		reply(403, 'AUTHENTICATION_FAILED');
 		return;
@@ -35,7 +37,7 @@ export default async (endpoint: Endpoint, ctx: Koa.Context) => {
 
 	// API invoking
 	try {
-		res = await call(endpoint, user, app, ctx.request.body, ctx.req);
+		res = await call(endpoint, user, app, body, (ctx.req as any).file);
 	} catch (e) {
 		reply(400, e);
 		return;
diff --git a/src/server/api/call.ts b/src/server/api/call.ts
index cc40294657..713add566a 100644
--- a/src/server/api/call.ts
+++ b/src/server/api/call.ts
@@ -1,4 +1,3 @@
-import * as http from 'http';
 import * as multer from 'koa-multer';
 
 import endpoints, { Endpoint } from './endpoints';
@@ -6,7 +5,7 @@ import limitter from './limitter';
 import { IUser } from '../../models/user';
 import { IApp } from '../../models/app';
 
-export default (endpoint: string | Endpoint, user: IUser, app: IApp, data: any, req?: http.IncomingMessage) => new Promise<any>(async (ok, rej) => {
+export default (endpoint: string | Endpoint, user: IUser, app: IApp, data: any, file?: any) => new Promise<any>(async (ok, rej) => {
 	const isSecure = user != null && app == null;
 
 	const ep = typeof endpoint == 'string' ? endpoints.find(e => e.name == endpoint) : endpoint;
@@ -36,8 +35,8 @@ export default (endpoint: string | Endpoint, user: IUser, app: IApp, data: any,
 
 	let exec = require(`${__dirname}/endpoints/${ep.name}`);
 
-	if (ep.withFile && req) {
-		exec = exec.bind(null, (req as multer.MulterIncomingMessage).file);
+	if (ep.withFile && file) {
+		exec = exec.bind(null, file);
 	}
 
 	let res;
diff --git a/src/server/api/common/signin.ts b/src/server/api/common/signin.ts
index f57c38414c..44e1336f27 100644
--- a/src/server/api/common/signin.ts
+++ b/src/server/api/common/signin.ts
@@ -3,7 +3,7 @@ import * as Koa from 'koa';
 import config from '../../../config';
 import { ILocalUser } from '../../../models/user';
 
-export default function(ctx: Koa.Context, user: ILocalUser, redirect: boolean) {
+export default function(ctx: Koa.Context, user: ILocalUser, redirect = false) {
 	const expires = 1000 * 60 * 60 * 24 * 365; // One Year
 	ctx.cookies.set('i', user.token, {
 		path: '/',
@@ -16,5 +16,7 @@ export default function(ctx: Koa.Context, user: ILocalUser, redirect: boolean) {
 
 	if (redirect) {
 		ctx.redirect(config.url);
+	} else {
+		ctx.status = 204;
 	}
 }
diff --git a/src/server/api/index.ts b/src/server/api/index.ts
index 2ea5fccb5b..009c99acae 100644
--- a/src/server/api/index.ts
+++ b/src/server/api/index.ts
@@ -14,7 +14,8 @@ const handler = require('./api-handler').default;
 // Init app
 const app = new Koa();
 app.use(bodyParser({
-	detectJSON: () => true
+	// リクエストが multipart/form-data でない限りはJSONだと見なす
+	detectJSON: ctx => !ctx.is('multipart/form-data')
 }));
 
 // Init multer instance
diff --git a/src/server/api/private/signin.ts b/src/server/api/private/signin.ts
index 1737007206..5450c7ad27 100644
--- a/src/server/api/private/signin.ts
+++ b/src/server/api/private/signin.ts
@@ -60,14 +60,14 @@ export default async (ctx: Koa.Context) => {
 			});
 
 			if (verified) {
-				signin(ctx, user, false);
+				signin(ctx, user);
 			} else {
 				ctx.throw(400, {
 					error: 'invalid token'
 				});
 			}
 		} else {
-			signin(ctx, user, false);
+			signin(ctx, user);
 		}
 	} else {
 		ctx.throw(400, {

From f3c4152a684b28b144caa7a609c98f695e7d04af Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 13 Apr 2018 11:50:36 +0900
Subject: [PATCH 26/34] Clean up

---
 package.json | 57 ++++++++++++++++++----------------------------------
 1 file changed, 19 insertions(+), 38 deletions(-)

diff --git a/package.json b/package.json
index b611f603c9..eed5d63702 100644
--- a/package.json
+++ b/package.json
@@ -30,21 +30,16 @@
 		"@fortawesome/fontawesome-free-brands": "5.0.2",
 		"@fortawesome/fontawesome-free-regular": "5.0.2",
 		"@fortawesome/fontawesome-free-solid": "5.0.2",
-		"@koa/cors": "^2.2.1",
+		"@koa/cors": "2.2.1",
 		"@prezzemolo/rap": "0.1.2",
 		"@prezzemolo/zip": "0.0.3",
 		"@types/bcryptjs": "2.4.1",
-		"@types/body-parser": "1.16.8",
 		"@types/chai": "4.1.2",
 		"@types/chai-http": "3.0.4",
-		"@types/compression": "0.0.36",
-		"@types/cookie": "0.3.1",
-		"@types/cors": "2.8.3",
 		"@types/debug": "0.0.30",
 		"@types/deep-equal": "1.0.1",
 		"@types/elasticsearch": "5.0.22",
 		"@types/eventemitter3": "2.0.2",
-		"@types/express": "4.11.1",
 		"@types/gm": "1.17.33",
 		"@types/gulp": "3.8.36",
 		"@types/gulp-htmlmin": "1.3.32",
@@ -57,26 +52,23 @@
 		"@types/is-root": "1.0.0",
 		"@types/is-url": "1.2.28",
 		"@types/js-yaml": "3.11.1",
-		"@types/koa": "^2.0.45",
-		"@types/koa-bodyparser": "^4.2.0",
-		"@types/koa-favicon": "^2.0.19",
-		"@types/koa-mount": "^3.0.1",
-		"@types/koa-multer": "^1.0.0",
-		"@types/koa-router": "^7.0.27",
-		"@types/koa-send": "^4.1.1",
-		"@types/koa__cors": "^2.2.2",
-		"@types/kue": "^0.11.8",
+		"@types/koa": "2.0.45",
+		"@types/koa-bodyparser": "4.2.0",
+		"@types/koa-favicon": "2.0.19",
+		"@types/koa-mount": "3.0.1",
+		"@types/koa-multer": "1.0.0",
+		"@types/koa-router": "7.0.27",
+		"@types/koa-send": "4.1.1",
+		"@types/koa__cors": "2.2.2",
+		"@types/kue": "0.11.8",
 		"@types/license-checker": "15.0.0",
 		"@types/mkdirp": "0.5.2",
 		"@types/mocha": "5.0.0",
 		"@types/mongodb": "3.0.12",
 		"@types/monk": "6.0.0",
-		"@types/morgan": "1.7.35",
 		"@types/ms": "0.7.30",
-		"@types/multer": "1.3.6",
 		"@types/node": "9.6.4",
 		"@types/nopt": "3.0.29",
-		"@types/proxy-addr": "2.0.0",
 		"@types/pug": "2.0.4",
 		"@types/qrcode": "0.8.1",
 		"@types/ratelimiter": "2.1.28",
@@ -85,7 +77,6 @@
 		"@types/request-promise-native": "1.0.14",
 		"@types/rimraf": "2.0.2",
 		"@types/seedrandom": "2.4.27",
-		"@types/serve-favicon": "2.2.30",
 		"@types/speakeasy": "2.0.2",
 		"@types/tmp": "0.0.33",
 		"@types/uuid": "3.4.3",
@@ -97,22 +88,18 @@
 		"autosize": "4.0.1",
 		"autwh": "0.1.0",
 		"bcryptjs": "2.4.3",
-		"body-parser": "1.18.2",
 		"bootstrap-vue": "2.0.0-rc.6",
 		"cafy": "3.2.1",
 		"chai": "4.1.2",
 		"chai-http": "4.0.0",
 		"chalk": "2.3.2",
-		"compression": "1.7.2",
-		"cookie": "0.3.1",
-		"cors": "2.8.4",
 		"crc-32": "1.2.0",
 		"css-loader": "0.28.11",
 		"debug": "3.1.0",
 		"deep-equal": "1.0.1",
 		"deepcopy": "0.6.3",
 		"diskusage": "0.2.4",
-		"dompurify": "^1.0.3",
+		"dompurify": "1.0.3",
 		"elasticsearch": "14.2.2",
 		"element-ui": "2.3.3",
 		"emojilib": "2.2.12",
@@ -121,7 +108,6 @@
 		"eslint-plugin-vue": "4.4.0",
 		"eventemitter3": "3.0.1",
 		"exif-js": "2.3.0",
-		"express": "4.16.3",
 		"file-loader": "1.1.11",
 		"file-type": "7.6.0",
 		"fuckadblock": "3.2.1",
@@ -143,19 +129,19 @@
 		"hard-source-webpack-plugin": "0.6.4",
 		"highlight.js": "9.12.0",
 		"html-minifier": "3.5.14",
-		"http-signature": "^1.2.0",
+		"http-signature": "1.2.0",
 		"inquirer": "5.2.0",
 		"is-root": "2.0.0",
 		"is-url": "1.2.4",
 		"js-yaml": "3.11.0",
 		"jsdom": "11.7.0",
-		"koa": "^2.5.0",
-		"koa-bodyparser": "^4.2.0",
-		"koa-favicon": "^2.0.1",
-		"koa-mount": "^3.0.0",
-		"koa-multer": "^1.0.2",
-		"koa-router": "^7.4.0",
-		"koa-send": "^4.1.3",
+		"koa": "2.5.0",
+		"koa-bodyparser": "4.2.0",
+		"koa-favicon": "2.0.1",
+		"koa-mount": "3.0.0",
+		"koa-multer": "1.0.2",
+		"koa-router": "7.4.0",
+		"koa-send": "4.1.3",
 		"kue": "0.11.6",
 		"license-checker": "18.0.0",
 		"loader-utils": "1.1.0",
@@ -165,9 +151,7 @@
 		"moji": "0.5.1",
 		"mongodb": "3.0.6",
 		"monk": "6.0.5",
-		"morgan": "1.9.0",
 		"ms": "2.1.1",
-		"multer": "1.3.0",
 		"nan": "2.10.0",
 		"node-sass": "4.8.3",
 		"node-sass-json-importer": "3.1.6",
@@ -178,7 +162,6 @@
 		"os-utils": "0.0.14",
 		"progress-bar-webpack-plugin": "1.11.0",
 		"prominence": "0.2.0",
-		"proxy-addr": "2.0.3",
 		"pug": "2.0.3",
 		"punycode": "2.1.0",
 		"qrcode": "1.2.0",
@@ -193,7 +176,6 @@
 		"s-age": "1.1.2",
 		"sass-loader": "6.0.7",
 		"seedrandom": "2.4.3",
-		"serve-favicon": "2.5.0",
 		"speakeasy": "2.0.0",
 		"style-loader": "0.20.3",
 		"stylus": "0.54.5",
@@ -213,7 +195,6 @@
 		"url-loader": "1.0.1",
 		"uuid": "3.2.1",
 		"v-animate-css": "0.0.2",
-		"vhost": "3.0.2",
 		"vue": "2.5.16",
 		"vue-cropperjs": "2.2.0",
 		"vue-js-modal": "1.3.12",

From b099ad2a30aac43c4dbe36864f9045a687cfd30b Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 13 Apr 2018 12:05:24 +0900
Subject: [PATCH 27/34] wip

---
 .../common/views/components/url-preview.vue   |  2 +-
 src/client/docs/layout.pug                    |  2 +-
 src/server/web/docs.ts                        | 20 ++++++++++++-------
 src/server/web/index.ts                       |  4 +++-
 4 files changed, 18 insertions(+), 10 deletions(-)

diff --git a/src/client/app/common/views/components/url-preview.vue b/src/client/app/common/views/components/url-preview.vue
index e91e510550..fd25480f61 100644
--- a/src/client/app/common/views/components/url-preview.vue
+++ b/src/client/app/common/views/components/url-preview.vue
@@ -45,7 +45,7 @@ export default Vue.extend({
 		} else if (url.hostname == 'youtu.be') {
 			this.youtubeId = url.pathname;
 		} else {
-			fetch('/api:url?url=' + this.url).then(res => {
+			fetch('/url?url=' + this.url).then(res => {
 				res.json().then(info => {
 					this.title = info.title;
 					this.description = info.description;
diff --git a/src/client/docs/layout.pug b/src/client/docs/layout.pug
index 29d2a3ff69..1d9ebcb4cd 100644
--- a/src/client/docs/layout.pug
+++ b/src/client/docs/layout.pug
@@ -6,7 +6,7 @@ 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/style.css")
+		link(rel="stylesheet" href="/docs/assets/style.css")
 		block meta
 
 		//- FontAwesome style
diff --git a/src/server/web/docs.ts b/src/server/web/docs.ts
index a546d1e88c..75da010682 100644
--- a/src/server/web/docs.ts
+++ b/src/server/web/docs.ts
@@ -2,20 +2,26 @@
  * Docs
  */
 
-import * as path from 'path';
+import ms = require('ms');
 import * as Router from 'koa-router';
 import * as send from 'koa-send';
 
-const docs = path.resolve(`${__dirname}/../../client/docs/`);
+const docs = `${__dirname}/../../client/docs/`;
 
 const router = new Router();
 
-router.get('/assets', async ctx => {
-	await send(ctx, `${docs}/assets`);
+router.get('/assets/*', async ctx => {
+	await send(ctx, ctx.path, {
+		root: docs,
+		maxage: ms('7 days'),
+		immutable: true
+	});
 });
 
-router.get(/^\/([a-z_\-\/]+?)$/, async ctx => {
-	await send(ctx, `${docs}/${ctx.params[0]}.html`);
+router.get('*', async ctx => {
+	await send(ctx, `${ctx.params[0]}.html`, {
+		root: docs
+	});
 });
 
-module.exports = router;
+export default router;
diff --git a/src/server/web/index.ts b/src/server/web/index.ts
index dd296f875d..376aadda73 100644
--- a/src/server/web/index.ts
+++ b/src/server/web/index.ts
@@ -8,6 +8,8 @@ import * as Router from 'koa-router';
 import * as send from 'koa-send';
 import * as favicon from 'koa-favicon';
 
+import docs from './docs';
+
 const client = `${__dirname}/../../client/`;
 
 // Init app
@@ -54,7 +56,7 @@ router.get('/manifest.json', async ctx => {
 //#endregion
 
 // Docs
-router.use('/docs', require('./docs').routes());
+router.use('/docs', docs.routes());
 
 // URL preview endpoint
 router.get('url', require('./url-preview'));

From f618dedfbc1471de19ea72a4ab6ffbad44dc7e56 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 13 Apr 2018 12:08:56 +0900
Subject: [PATCH 28/34] Fix

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

diff --git a/src/server/web/docs.ts b/src/server/web/docs.ts
index 75da010682..e65cc87b12 100644
--- a/src/server/web/docs.ts
+++ b/src/server/web/docs.ts
@@ -11,8 +11,8 @@ const docs = `${__dirname}/../../client/docs/`;
 const router = new Router();
 
 router.get('/assets/*', async ctx => {
-	await send(ctx, ctx.path, {
-		root: docs,
+	await send(ctx, ctx.params[0], {
+		root: docs + '/assets/',
 		maxage: ms('7 days'),
 		immutable: true
 	});

From 645481c2e8d13123c74271fa86291f10b99b9a55 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 13 Apr 2018 13:36:21 +0900
Subject: [PATCH 29/34] Use http2

---
 src/server/index.ts | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)

diff --git a/src/server/index.ts b/src/server/index.ts
index a637e8598b..5f6d3a84df 100644
--- a/src/server/index.ts
+++ b/src/server/index.ts
@@ -3,7 +3,6 @@
  */
 
 import * as fs from 'fs';
-import * as http from 'http';
 import * as http2 from 'http2';
 import * as Koa from 'koa';
 import * as Router from 'koa-router';
@@ -49,7 +48,7 @@ function createServer() {
 		});
 		return http2.createSecureServer(certs, app.callback());
 	} else {
-		return http.createServer(app.callback());
+		return http2.createServer(app.callback());
 	}
 }
 

From 732e07a4b3800a7d5527a8be7331cd0ef44fdc85 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 13 Apr 2018 13:52:25 +0900
Subject: [PATCH 30/34] Clean up

---
 src/server/api/call.ts | 2 --
 1 file changed, 2 deletions(-)

diff --git a/src/server/api/call.ts b/src/server/api/call.ts
index 713add566a..fd3cea7743 100644
--- a/src/server/api/call.ts
+++ b/src/server/api/call.ts
@@ -1,5 +1,3 @@
-import * as multer from 'koa-multer';
-
 import endpoints, { Endpoint } from './endpoints';
 import limitter from './limitter';
 import { IUser } from '../../models/user';

From 16920caf9cf5340f8e1ea3b40ff145967fc72c0a Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 13 Apr 2018 14:07:42 +0900
Subject: [PATCH 31/34] Revert "Use http2"

This reverts commit 645481c2e8d13123c74271fa86291f10b99b9a55.
---
 src/server/index.ts | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/src/server/index.ts b/src/server/index.ts
index 5f6d3a84df..a637e8598b 100644
--- a/src/server/index.ts
+++ b/src/server/index.ts
@@ -3,6 +3,7 @@
  */
 
 import * as fs from 'fs';
+import * as http from 'http';
 import * as http2 from 'http2';
 import * as Koa from 'koa';
 import * as Router from 'koa-router';
@@ -48,7 +49,7 @@ function createServer() {
 		});
 		return http2.createSecureServer(certs, app.callback());
 	} else {
-		return http2.createServer(app.callback());
+		return http.createServer(app.callback());
 	}
 }
 

From 8e0949c66f5108053ebbdeb3381c358bc63f8178 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 13 Apr 2018 14:28:09 +0900
Subject: [PATCH 32/34] compress

---
 package.json        | 2 ++
 src/server/index.ts | 6 ++++++
 2 files changed, 8 insertions(+)

diff --git a/package.json b/package.json
index eed5d63702..d82e5d2330 100644
--- a/package.json
+++ b/package.json
@@ -54,6 +54,7 @@
 		"@types/js-yaml": "3.11.1",
 		"@types/koa": "2.0.45",
 		"@types/koa-bodyparser": "4.2.0",
+		"@types/koa-compress": "^2.0.8",
 		"@types/koa-favicon": "2.0.19",
 		"@types/koa-mount": "3.0.1",
 		"@types/koa-multer": "1.0.0",
@@ -137,6 +138,7 @@
 		"jsdom": "11.7.0",
 		"koa": "2.5.0",
 		"koa-bodyparser": "4.2.0",
+		"koa-compress": "^2.0.0",
 		"koa-favicon": "2.0.1",
 		"koa-mount": "3.0.0",
 		"koa-multer": "1.0.2",
diff --git a/src/server/index.ts b/src/server/index.ts
index a637e8598b..db41a1dcb5 100644
--- a/src/server/index.ts
+++ b/src/server/index.ts
@@ -5,9 +5,11 @@
 import * as fs from 'fs';
 import * as http from 'http';
 import * as http2 from 'http2';
+import * as zlib from 'zlib';
 import * as Koa from 'koa';
 import * as Router from 'koa-router';
 import * as mount from 'koa-mount';
+import * as compress from 'koa-compress';
 
 import activityPub from './activitypub';
 import webFinger from './webfinger';
@@ -17,6 +19,10 @@ import config from '../config';
 const app = new Koa();
 app.proxy = true;
 
+app.use(compress({
+	flush: zlib.constants.Z_SYNC_FLUSH
+}));
+
 // HSTS
 // 6months (15552000sec)
 if (config.url.startsWith('https')) {

From a9080f87ad07c1960e7865801f9fdea5b8e8514a Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 13 Apr 2018 14:29:01 +0900
Subject: [PATCH 33/34] Fix bug

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

diff --git a/src/server/activitypub.ts b/src/server/activitypub.ts
index ed0311af9d..acd10b7886 100644
--- a/src/server/activitypub.ts
+++ b/src/server/activitypub.ts
@@ -44,7 +44,7 @@ router.post('/users/:user/inbox', ctx => {
 router.get('/notes/:note', async (ctx, next) => {
 	const accepted = ctx.accepts('html', 'application/activity+json', 'application/ld+json');
 	if (!['application/activity+json', 'application/ld+json'].includes(accepted as string)) {
-		next();
+		await next();
 		return;
 	}
 

From 17f4dd69a38097d92d60829e37cacdbb27b61f94 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 13 Apr 2018 14:39:08 +0900
Subject: [PATCH 34/34] Refactor

---
 src/remote/activitypub/renderer/context.ts |  5 -----
 src/remote/activitypub/renderer/index.ts   |  7 +++++++
 src/server/activitypub.ts                  | 21 +++++----------------
 src/services/following/create.ts           | 10 +++-------
 src/services/following/delete.ts           |  6 ++----
 src/services/note/create.ts                |  5 ++---
 src/services/note/reaction/create.ts       |  6 ++----
 7 files changed, 21 insertions(+), 39 deletions(-)
 delete mode 100644 src/remote/activitypub/renderer/context.ts
 create mode 100644 src/remote/activitypub/renderer/index.ts

diff --git a/src/remote/activitypub/renderer/context.ts b/src/remote/activitypub/renderer/context.ts
deleted file mode 100644
index b56f727ae7..0000000000
--- a/src/remote/activitypub/renderer/context.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-export default [
-	'https://www.w3.org/ns/activitystreams',
-	'https://w3id.org/security/v1',
-	{ Hashtag: 'as:Hashtag' }
-];
diff --git a/src/remote/activitypub/renderer/index.ts b/src/remote/activitypub/renderer/index.ts
new file mode 100644
index 0000000000..ee7f496162
--- /dev/null
+++ b/src/remote/activitypub/renderer/index.ts
@@ -0,0 +1,7 @@
+export default (x: any) => Object.assign({
+	'@context': [
+		'https://www.w3.org/ns/activitystreams',
+		'https://w3id.org/security/v1',
+		{ Hashtag: 'as:Hashtag' }
+	]
+}, x);
diff --git a/src/server/activitypub.ts b/src/server/activitypub.ts
index acd10b7886..2a99bccfc4 100644
--- a/src/server/activitypub.ts
+++ b/src/server/activitypub.ts
@@ -2,8 +2,7 @@ import * as Router from 'koa-router';
 import { parseRequest } from 'http-signature';
 
 import { createHttp } from '../queue';
-import context from '../remote/activitypub/renderer/context';
-import render from '../remote/activitypub/renderer/note';
+import pack from '../remote/activitypub/renderer';
 import Note from '../models/note';
 import User, { isLocalUser } from '../models/user';
 import renderNote from '../remote/activitypub/renderer/note';
@@ -57,10 +56,7 @@ router.get('/notes/:note', async (ctx, next) => {
 		return;
 	}
 
-	const rendered = await render(note);
-	rendered['@context'] = context;
-
-	ctx.body = rendered;
+	ctx.body = pack(await renderNote(note));
 });
 
 // outbot
@@ -81,9 +77,8 @@ router.get('/users/:user/outbox', async ctx => {
 
 	const renderedNotes = await Promise.all(notes.map(note => renderNote(note)));
 	const rendered = renderOrderedCollection(`${config.url}/users/${userId}/inbox`, user.notesCount, renderedNotes);
-	rendered['@context'] = context;
 
-	ctx.body = rendered;
+	ctx.body = pack(rendered);
 });
 
 // publickey
@@ -98,10 +93,7 @@ router.get('/users/:user/publickey', async ctx => {
 	}
 
 	if (isLocalUser(user)) {
-		const rendered = renderKey(user);
-		rendered['@context'] = context;
-
-		ctx.body = rendered;
+		ctx.body = pack(renderKey(user));
 	} else {
 		ctx.status = 400;
 	}
@@ -118,10 +110,7 @@ router.get('/users/:user', async ctx => {
 		return;
 	}
 
-	const rendered = renderPerson(user);
-	rendered['@context'] = context;
-
-	ctx.body = rendered;
+	ctx.body = pack(renderPerson(user));
 });
 
 // follow form
diff --git a/src/services/following/create.ts b/src/services/following/create.ts
index 31e3be19ed..3289e31294 100644
--- a/src/services/following/create.ts
+++ b/src/services/following/create.ts
@@ -4,7 +4,7 @@ import FollowingLog from '../../models/following-log';
 import FollowedLog from '../../models/followed-log';
 import event from '../../publishers/stream';
 import notify from '../../publishers/notify';
-import context from '../../remote/activitypub/renderer/context';
+import pack from '../../remote/activitypub/renderer';
 import renderFollow from '../../remote/activitypub/renderer/follow';
 import renderAccept from '../../remote/activitypub/renderer/accept';
 import { deliver } from '../../queue';
@@ -57,16 +57,12 @@ export default async function(follower: IUser, followee: IUser, activity?) {
 	}
 
 	if (isLocalUser(follower) && isRemoteUser(followee)) {
-		const content = renderFollow(follower, followee);
-		content['@context'] = context;
-
+		const content = pack(renderFollow(follower, followee));
 		deliver(follower, content, followee.inbox).save();
 	}
 
 	if (isRemoteUser(follower) && isLocalUser(followee)) {
-		const content = renderAccept(activity);
-		content['@context'] = context;
-
+		const content = pack(renderAccept(activity));
 		deliver(followee, content, follower.inbox).save();
 	}
 }
diff --git a/src/services/following/delete.ts b/src/services/following/delete.ts
index d79bf64f53..8b6c56816a 100644
--- a/src/services/following/delete.ts
+++ b/src/services/following/delete.ts
@@ -3,7 +3,7 @@ import Following from '../../models/following';
 import FollowingLog from '../../models/following-log';
 import FollowedLog from '../../models/followed-log';
 import event from '../../publishers/stream';
-import context from '../../remote/activitypub/renderer/context';
+import pack from '../../remote/activitypub/renderer';
 import renderFollow from '../../remote/activitypub/renderer/follow';
 import renderUndo from '../../remote/activitypub/renderer/undo';
 import { deliver } from '../../queue';
@@ -56,9 +56,7 @@ export default async function(follower: IUser, followee: IUser, activity?) {
 	}
 
 	if (isLocalUser(follower) && isRemoteUser(followee)) {
-		const content = renderUndo(renderFollow(follower, followee));
-		content['@context'] = context;
-
+		const content = pack(renderUndo(renderFollow(follower, followee)));
 		deliver(follower, content, followee.inbox).save();
 	}
 }
diff --git a/src/services/note/create.ts b/src/services/note/create.ts
index 8f0b84bccd..b238cd5607 100644
--- a/src/services/note/create.ts
+++ b/src/services/note/create.ts
@@ -6,7 +6,7 @@ import { deliver } from '../../queue';
 import renderNote from '../../remote/activitypub/renderer/note';
 import renderCreate from '../../remote/activitypub/renderer/create';
 import renderAnnounce from '../../remote/activitypub/renderer/announce';
-import context from '../../remote/activitypub/renderer/context';
+import packAp from '../../remote/activitypub/renderer';
 import { IDriveFile } from '../../models/drive-file';
 import notify from '../../publishers/notify';
 import NoteWatching from '../../models/note-watching';
@@ -132,8 +132,7 @@ export default async (user: IUser, data: {
 				const content = data.renote && data.text == null
 					? renderAnnounce(data.renote.uri ? data.renote.uri : await renderNote(data.renote))
 					: renderCreate(await renderNote(note));
-				content['@context'] = context;
-				return content;
+				return packAp(content);
 			};
 
 			// 投稿がリプライかつ投稿者がローカルユーザーかつリプライ先の投稿の投稿者がリモートユーザーなら配送
diff --git a/src/services/note/reaction/create.ts b/src/services/note/reaction/create.ts
index 88158034f3..69a14248da 100644
--- a/src/services/note/reaction/create.ts
+++ b/src/services/note/reaction/create.ts
@@ -8,7 +8,7 @@ import NoteWatching from '../../../models/note-watching';
 import watch from '../watch';
 import renderLike from '../../../remote/activitypub/renderer/like';
 import { deliver } from '../../../queue';
-import context from '../../../remote/activitypub/renderer/context';
+import pack from '../../../remote/activitypub/renderer';
 
 export default async (user: IUser, note: INote, reaction: string) => new Promise(async (res, rej) => {
 	// Myself
@@ -85,9 +85,7 @@ export default async (user: IUser, note: INote, reaction: string) => new Promise
 	//#region 配信
 	// リアクターがローカルユーザーかつリアクション対象がリモートユーザーの投稿なら配送
 	if (isLocalUser(user) && isRemoteUser(note._user)) {
-		const content = renderLike(user, note);
-		content['@context'] = context;
-
+		const content = pack(renderLike(user, note));
 		deliver(user, content, note._user.inbox).save();
 	}
 	//#endregion